`: the delta of
`runtime.MemStats.NumGC` across the workload, read via
`runtime.ReadMemStats`. C++ and Rust have no collector, so the field would
- be a lie there — its absence is part of the lesson.
+ be a lie there; its absence is part of the lesson.
- Bad usage (unknown flag, non-positive `--allocs`, unknown variant) prints
a diagnostic plus `usage: allocbench [--allocs N] [--variant default|arena]`
on stderr and exits 2. Runtime failures print `allocbench: ...` on stderr
and exit 1.
- The query phase is a real check: if any of the distinct keys is missing
- from the index or the summed value bytes are zero, the run fails — the
+ from the index or the summed value bytes are zero, the run fails: the
benchmark cannot "pass" by optimizing the work away.
## The two variants, per language
@@ -41,21 +41,21 @@ allocbench [--allocs N] [--variant default|arena]
| | default | arena |
|---|---|---|
| **C++** | `std::string` intermediates from the global heap, freed one by one when each batch's scratch `std::vector` is destroyed | per-batch `std::pmr::monotonic_buffer_resource` over one reusable 256 KiB slab; intermediates are `std::pmr::string`s held in a `std::pmr::vector`, released wholesale when the resource is destroyed |
-| **Go** | fresh strings every iteration (`strconv.Itoa`, `+` concatenation) — all of it garbage for the collector | `sync.Pool` of scratch objects (three `bytes.Buffer`s plus a `strconv.AppendInt` byte slice): churn buffers are recycled, not collected |
+| **Go** | fresh strings every iteration (`strconv.Itoa`, `+` concatenation): all of it garbage for the collector | `sync.Pool` of scratch objects (three `bytes.Buffer`s plus a `strconv.AppendInt` byte slice): churn buffers are recycled, not collected |
| **Rust** | `String`/`format!` intermediates from the global allocator, dropped individually | `bumpalo::Bump` arena: `bumpalo::collections::String`/`Vec` intermediates, reclaimed in O(1) by `bump.reset()` after each 1000-iteration batch |
The long-lived index itself is an ordinary hash map in all six builds; only
the final key/value pairs are copied into it. That is the arena discipline
in one sentence: bulk-free the churn, copy out what survives.
-Go deliberately is not "arena" in the C++/Rust sense — the language offers
+Go is not "arena" in the C++/Rust sense: the language offers
no stable arena API (the `arena` experiment stalled), so the idiomatic
answer to allocation churn is object reuse via `sync.Pool`, which is what
the variant demonstrates.
## Knobs worth playing with (Go)
-The Go runtime exposes its memory policy as environment variables — no
+The Go runtime exposes its memory policy as environment variables, no
rebuild required:
```console
@@ -71,9 +71,9 @@ allocbench: variant=default allocs=200000 peak_rss=37292KB ms=51 gc_cycles=0
```
`GOMEMLIMIT` (Go 1.19+) is a soft cap on the runtime's total memory: as the
-heap approaches it, the collector runs more aggressively to stay under —
-and if the limit is never approached (last run above), the collector simply
-never fires and the garbage piles up as RSS. Watch `gc_cycles` and
+heap approaches it, the collector runs more aggressively to stay under; if
+the limit is never approached (last run above), the collector never fires
+and the garbage piles up as RSS. Watch `gc_cycles` and
`peak_rss` move in opposite directions as you trade one for the other.
`GOGC` scales how much new garbage may accumulate between cycles (default
100 = the heap may double).
@@ -93,9 +93,9 @@ $ ./demo.sh rust run --variant arena
allocbench: variant=arena allocs=200000 peak_rss=2576KB ms=31
```
-(Numbers vary by host and run; only the line shape is stable.) Note what
-the C++ pair shows: at this workload size the arena's peak RSS is
-*comparable*, not lower — glibc malloc recycles the churn through the same
+(Numbers vary by host and run; only the line shape is stable.) The C++ pair
+shows that at this workload size the arena's peak RSS is
+*comparable*, not lower: glibc malloc recycles the churn through the same
pages, while the arena's always-touched 256 KiB slab is a fixed cost. The
arena's win here is deterministic batch reclamation (no per-object frees),
not a smaller high-water mark; crank `--allocs` or the value sizes and the
@@ -105,8 +105,8 @@ curves separate.
Each language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (builds first if needed)
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (builds first if needed)
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
@@ -118,9 +118,9 @@ The top-level `demo.sh` dispatches: `./demo.sh go run --variant arena` execs
`verify.lua` (run per language with `LSP_LANG` set) asserts behavior, not
just exit codes: bad usage exits 2 with the usage line; both variants emit
the exact single-line report; `variant`/`allocs` echo the request;
-`peak_rss`/`ms` (and `gc_cycles` for Go — which must be absent for C++ and
+`peak_rss`/`ms` (and `gc_cycles` for Go, which must be absent for C++ and
Rust) parse to sane numbers; the bare run defaults to
`variant=default allocs=200000`; and a 7-alloc run still answers its
queries. The default-vs-arena RSS delta is printed to the log but not
-asserted — ordering can flake on a loaded host, and the chapter text, not
+asserted: ordering can flake on a loaded host, and the chapter text, not
CI, owns that claim.
diff --git a/examples/18-pipes-fifos-splice/README.md b/examples/18-pipes-fifos-splice/README.md
index 185c2d3..e37dd19 100644
--- a/examples/18-pipes-fifos-splice/README.md
+++ b/examples/18-pipes-fifos-splice/README.md
@@ -1,30 +1,30 @@
-# 18 — Pipes, FIFOs, splice (`pmon` v4)
+# 18: Pipes, FIFOs, splice (`pmon` v4)
`pmon` is the process supervisor that grows across the book: v0 `run`,
-v1 the sigchld supervise engine, v2 the pidfd engine (chapter 13), v3 the
+v1 the sigchld supervise engine, v2 the pidfd engine, v3 the
privilege-drop chapter. v4 keeps both supervise engines exactly as they were
and turns the supervisor into a log pipeline built from kernel pipe
machinery:
-- **`supervise` captures the child** — the child's stdout and stderr each
+- **`supervise` captures the child**: the child's stdout and stderr each
flow through their own `pipe(2)` back into the supervisor, which relays
them line-buffered into a log file as `[out] ...` / `[err] ...` lines.
- The two pipe read ends simply join the fd set each engine already
+ The two pipe read ends join the fd set each engine already
multiplexes; nothing about the v1/v2 restart/timeout/stop behavior
changes.
-- **`tail` republishes the log** — it creates a FIFO (`mkfifo(3)`), follows
+- **`tail` republishes the log**: it creates a FIFO (`mkfifo(3)`), follows
the log file, and relays appended bytes into whatever reader opens the
FIFO. C++ and Rust move the bytes kernel-side with `splice(2)`
(file → pipe, no userspace copy; automatic `read`/`write` fallback on
filesystems without splice support). A reader that disappears is not
fatal: SIGPIPE is ignored, the resulting `EPIPE` prints
- `pmon: tail reader detached`, and the relay waits for the next reader —
- losing nothing, because the log offset only advances for bytes that
- actually reached the pipe.
+ `pmon: tail reader detached`, and the relay waits for the next reader,
+ losing nothing because the log offset only advances for bytes that
+ reached the pipe.
| Language | Architecture |
|---|---|
-| **C++23** | one `poll(2)` loop per child over four fds — the two capture pipes, the signalfd, and (pidfd engine) the pidfd from raw `SYS_pidfd_open`; `waitid(P_PIDFD)` reaps race-free. RAII `Fd`/`SpawnAttrs`/`FileActions` owners, `std::expected` error paths, `posix_spawnp` wiring the pipes via `adddup2` and resetting the child's signal mask. `tail` uses the `splice(2)` fast path with an offset-preserving `read`/`write` fallback. |
+| **C++23** | one `poll(2)` loop per child over four fds: the two capture pipes, the signalfd, and (pidfd engine) the pidfd from raw `SYS_pidfd_open`; `waitid(P_PIDFD)` reaps race-free. RAII `Fd`/`SpawnAttrs`/`FileActions` owners, `std::expected` error paths, `posix_spawnp` wiring the pipes via `adddup2` and resetting the child's signal mask. `tail` uses the `splice(2)` fast path with an offset-preserving `read`/`write` fallback. |
| **Rust (edition 2024)** | the same poll set via `rustix::event::poll` + a nix `SignalFd`, `pidfd_open` returning an `OwnedFd`, `waitid(WaitId::PidFd)`; `rustix::pipe::splice` for the tail fast path; `anyhow` + `?` throughout, every fd `OwnedFd`-backed. |
| **Go 1.26** | the idiomatic inversion: one goroutine per capture pipe feeds a channel of prefixed lines into the same `select` that already multiplexes the pidfd poller (or `signal.Notify(SIGCHLD)`), the stop signals, and the deadline. `tail` relays with `io.Copy` per reader session, rewinding the log to `start+written` on `EPIPE`; `mkfifo`/nonblocking FIFO open via `golang.org/x/sys/unix`. |
@@ -60,7 +60,7 @@ New in v4:
`[out] line` / `[err] line`; a trailing partial line is flushed with a
newline when the stream closes, and the log is fully drained before the
exit observation is printed.
-- `tail --log FILE --fifo PATH` — creates PATH as a FIFO (an existing FIFO
+- `tail --log FILE --fifo PATH`: creates PATH as a FIFO (an existing FIFO
is reused; anything else is an error), prints
`pmon: tail ready (fifo PATH)` to stderr, then relays FILE from the
beginning into each successive FIFO reader, polling for appended bytes.
@@ -73,11 +73,11 @@ New in v4:
Each language directory has the standard `demo.sh`:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (`app`); the local path
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (`app`); the local path
`exec`s the binary so a backgrounded `run tail ...` can be signalled
directly
-- `./demo.sh` — build, then a one-shot self-demo (`run -- sh -c ...`)
+- `./demo.sh`: build, then a one-shot self-demo (`run -- sh -c ...`)
- With env `TARGET` set, `run` deploys to that lab VM via
`scripts/lab/deploy-to-vm.sh`
diff --git a/examples/19-unix-sockets-fd-passing/README.md b/examples/19-unix-sockets-fd-passing/README.md
index 0737cbf..e5ef308 100644
--- a/examples/19-unix-sockets-fd-passing/README.md
+++ b/examples/19-unix-sockets-fd-passing/README.md
@@ -1,8 +1,8 @@
-# 19-unix-sockets-fd-passing — pmon v5
+# 19-unix-sockets-fd-passing: pmon v5
pmon grows a control plane. `pmon supervise` runs a child command under
-supervision — own process group, stdout+stderr appended to a log file,
-automatic restart 300 ms after every exit — and listens on a UNIX stream
+supervision (own process group, stdout+stderr appended to a log file,
+automatic restart 300 ms after every exit) and listens on a UNIX stream
socket. `pmon pmctl` (same binary, different subcommand) talks to it:
```
@@ -25,9 +25,9 @@ $ ./demo.sh cpp run pmctl --ctl /tmp/p/ctl.sock stop
stopping
```
-## What the chapter is really about
+## The point of this chapter
-`logfd` is the headline. The supervisor does not send the log *path* — it
+`logfd` is the headline. The supervisor does not send the log *path*; it
sends its **open log file descriptor** through the socket in an `SCM_RIGHTS`
control message, and pmctl reads the last 3 lines directly off the received
fd (positional reads from offset 0, so the supervisor's `O_APPEND` writer is
@@ -38,11 +38,11 @@ by diffing the `via-fd:` output against `tail -3` of the file, byte for byte.
Two supporting ideas:
-- **SO_PEERCRED** — before trusting a single command byte, the supervisor
+- **SO_PEERCRED**: before trusting a single command byte, the supervisor
asks the kernel who is on the other end of the connection and logs
`pmon: ctl connect uid= pid=`. Credentials from the kernel, not from
the client's own claims.
-- **The log is opened O_RDWR|O_APPEND** — a write-only fd would arrive
+- **The log is opened O_RDWR|O_APPEND**: a write-only fd would arrive
write-only in pmctl (the *file description* crosses the socket, flags and
all), and the read side would fail with EBADF.
@@ -58,13 +58,13 @@ plus cmsg) out.
| **Go 1.26** | `unix.UnixRights` / `unix.ParseSocketControlMessage` / `unix.ParseUnixRights` over `net.UnixConn.WriteMsgUnix`/`ReadMsgUnix` | reaper goroutine; restart backoff vs. shutdown is a `select` on a timer and a stop channel; wrapped errors end at `pmon: error:` |
| **Rust (2024)** | `nix::sys::socket` `sendmsg`/`recvmsg` with `ControlMessage::ScmRights` and `cmsg_space!` | reaper thread, `Arc<(Mutex, Condvar)>`; the received fd goes straight into an `OwnedFd` so it closes on drop |
-## Gotchas worth remembering
+## Points worth remembering
- **`sun_path` is 108 bytes.** Bind/connect fail with `EINVAL`-flavored
errors when the socket path is longer (deep CI workspace paths hit this
immediately). Keep control sockets under short directories.
- Passing an fd duplicates the *open file description*, so offset and status
- flags travel with it — hence O_RDWR above, and hence pmctl using `pread`
+ flags travel with it: hence O_RDWR above, and hence pmctl using `pread`
instead of seeking, which would move the shared offset.
- The child gets its own process group (`setpgid`), so `stop` can SIGTERM
the whole tree, including anything the child forked.
diff --git a/examples/20-shared-memory-coordination/README.md b/examples/20-shared-memory-coordination/README.md
index 2d25537..40b9f56 100644
--- a/examples/20-shared-memory-coordination/README.md
+++ b/examples/20-shared-memory-coordination/README.md
@@ -1,9 +1,9 @@
-# 20-shared-memory-coordination — shmkv v1
+# 20-shared-memory-coordination: shmkv v1
shmkv grows from a passive shared-memory key-value file (v0) into a live
coordination fabric: one process publishes updates, other processes learn
about them **without polling**, through a seqlock for consistency and a futex
-for wakeups — the same pair of primitives glibc builds mutexes and condition
+for wakeups: the same pair of primitives glibc builds mutexes and condition
variables from. Implemented three times (C++23, Go 1.26, Rust 2024) with
identical observable behavior.
@@ -40,7 +40,7 @@ shmkv bench FILE [--rounds N] [--channel futex|mq|poll]
`observed update : =` for every new id, exiting 0 after N
events (or 1 after a 30 s self-termination deadline).
- **bench**: producer/consumer threads over the same mapping measure one-way
- notification latency for three channels — `futex` (wake/wait), `mq` (a
+ notification latency for three channels: `futex` (wake/wait), `mq` (a
POSIX message queue carrying the update id), `poll` (a 1 ms sleep loop
re-reading the counter; it sleeps *before* looking, so every observation
costs at least one full nap). Rounds are strictly serialized by an ack word
@@ -60,15 +60,15 @@ read: s1 = seq (acquire) ; if s1 odd retry
s2 = seq (relaxed) ; consistent iff s1 == s2, else retry
```
-The data copy races the writer on purpose; a torn copy is detected by the
-seq re-check and discarded. That trade-off — writers never block, readers
-retry — is exactly the kernel's seqlock. The retry loop is bounded (10^6
+The data copy races the writer by design; a torn copy is detected by the
+seq re-check and discarded. That trade-off (writers never block, readers
+retry) is exactly the kernel's seqlock. The retry loop is bounded (10^6
attempts) so a corrupt file cannot livelock a reader forever.
The futex is **shared** (no `FUTEX_PRIVATE_FLAG`): the kernel keys waiters
by the file's inode, which is what lets two unrelated processes rendezvous
through the same mapped page. `EAGAIN`, `ETIMEDOUT`, and `EINTR` from
-`FUTEX_WAIT` are all normal — the waiter re-reads shared state and decides
+`FUTEX_WAIT` are all normal: the waiter re-reads shared state and decides
again.
## Language notes
diff --git a/examples/21-tcp-sockets/README.md b/examples/21-tcp-sockets/README.md
index fcc1ee3..8e0a1df 100644
--- a/examples/21-tcp-sockets/README.md
+++ b/examples/21-tcp-sockets/README.md
@@ -1,4 +1,4 @@
-# 21-tcp-sockets — chatterd v0
+# 21-tcp-sockets: chatterd v0
`chatterd` is the book's peer-to-peer chat program. This first cut is a
thread-per-connection TCP **chat room**: one binary is both the daemon and the
@@ -32,10 +32,10 @@ that arrives as `: `.
## The wire protocol (fixed for the whole book)
-This chapter introduces **the** chatterd chat frame: chapters 22, 24, and 27
+This chapter introduces **the** chatterd chat frame: later chatterd versions
reuse this exact header and only ever *add* frame types, never change it. A
frame that a `chatctl` from this chapter reads or writes is byte-identical to
-the one a v1 (ch22), v3 (ch24), or v4 (ch27) build would produce.
+the one a later chatterd build would produce.
```
byte: 0 1 2 3 4 5 6 6+L-1
@@ -45,12 +45,12 @@ the one a v1 (ch22), v3 (ch24), or v4 (ch27) build would produce.
+----+----+------+------+----+----+---------------------------+
```
-- **magic** — the two bytes `0x43 0x48` (ASCII `"CH"`). Any other value means
+- **magic**: the two bytes `0x43 0x48` (ASCII `"CH"`). Any other value means
"not a chatterd frame"; the reader closes the connection.
-- **version** — one byte, `0x01`. This chapter is version 0 of the *program*
+- **version**: one byte, `0x01`. This chapter is version 0 of the *program*
but the wire's version byte has always been `1`; only the frame *header*
would ever bump it, and it never has.
-- **type** — one byte selecting the payload's shape. This chapter uses three
+- **type**: one byte selecting the payload's shape. This chapter uses three
of the five book-wide types:
| Type | Value | Payload | Who sends it | Meaning |
@@ -59,13 +59,14 @@ the one a v1 (ch22), v3 (ch24), or v4 (ch27) build would produce.
| **MSG** | `2` | `text` | client, per stdin line | "I say `text`" |
| **DELIVER** | `3` | `name` `00` `text` | server, to every client | a broadcast |
- `WELCOME` (`4`) and `PING` (`5`) are reserved for chapters 22 and 24; this
- program never sends either, and a `chatctl` reader here skips any frame type
- it does not recognize instead of erroring out — which is exactly what lets a
- ch21 client stay connected to a newer server that does send them.
-- **length** — a 2-byte unsigned payload length in **big-endian** order,
+ `WELCOME` (`4`) and `PING` (`5`) are reserved for later chatterd versions;
+ this program never sends either, and a `chatctl` reader here skips any
+ frame type it does not recognize instead of erroring out, which is exactly
+ what lets a ch21 client stay connected to a newer server that does send
+ them.
+- **length**: a 2-byte unsigned payload length in **big-endian** order,
`0..65535`. It counts the payload only, never the 6 header bytes.
-- **payload** — `length` bytes of UTF-8, shaped per the `type` above. A
+- **payload**: `length` bytes of UTF-8, shaped per the `type` above. A
`DELIVER` payload's `name` and `text` are separated by a single `0x00` NUL
byte; neither field may itself contain a NUL.
@@ -79,8 +80,8 @@ the reserved string `server`:
So a client that reads a `DELIVER` payload `server\0alice joined` prints
`server: alice joined`, and one that reads `alice\0hello bob` prints
-`alice: hello bob`. That is the whole protocol for this chapter; chapters
-22–24 and 27 add `WELCOME`/`PING` and new engines around it, never a new
+`alice: hello bob`. That is the whole protocol for this chapter; later
+chapters add `WELCOME`/`PING` and new engines around it, never a new
header.
### Worked bytes
@@ -129,7 +130,7 @@ second JOIN).
## The three implementations
-Identical CLI, identical stdout/stderr shapes, identical exit codes — one
+Identical CLI, identical stdout/stderr shapes, identical exit codes: one
`verify.lua` covers all three.
| | connection model | listener shutdown | fd ownership |
@@ -138,10 +139,10 @@ Identical CLI, identical stdout/stderr shapes, identical exit codes — one
| **Go 1.26** | one goroutine per client + a per-client writer goroutine draining a channel; a `select` over send/`done`/`ctx` never wedges a broadcaster | a `context` cancelled on SIGINT; `ln.Close()` unblocks `Accept` | `net.Conn` / `net.Listener` |
| **Rust 2024** | one `std::thread` per client; broadcasts write to `try_clone`d `TcpStream`s under a `Mutex` | `AtomicBool` set by the SIGINT handler, polled by a non-blocking `accept` loop | listener built via `nix` and taken as an `OwnedFd`; `TcpStream` owns each connection |
-No async runtime here on purpose — this chapter is about blocking sockets and
-threads. Chapter 27 is where `chatterd` meets tokio.
+This chapter covers blocking sockets and threads; an async runtime comes
+later, when `chatterd` meets tokio.
-## Gotchas worth remembering
+## Details worth remembering
- **Length prefix, not newline.** TCP is a byte stream with no message
boundaries; a single `read` may return half a frame or two frames. Every
@@ -150,7 +151,7 @@ threads. Chapter 27 is where `chatterd` meets tokio.
do.
- **Broadcast excludes the sender; join notices do not.** A speaker already
sees what they typed, so their own message is not echoed back. A join,
- though, goes to every member — which is why both clients in a two-party room
+ though, goes to every member, which is why both clients in a two-party room
each see a join line.
- **SIGINT and a blocking accept.** A flag set in the handler is invisible to a
thread parked in `accept`. C++ interrupts `poll` with the signal; Go closes
@@ -173,7 +174,7 @@ cpp/ go/ rust/ # one chatterd each; binary always ./…/app; demo.sh contract id
`python3 scripts/test-all-examples.py --only 21-tcp-sockets` builds all three
and runs `verify.lua` per language. The checks are behavioral: two real
`chatctl` clients are driven over loopback through scripted stdin fifos, and the
-test asserts the **actually delivered lines** — bob receives `alice: hello`,
+test asserts the **delivered lines**: bob receives `alice: hello`,
alice sees no echo of her own message, both clients see a join notice from the
`server` sender, the daemon logs each join, and SIGINT leaves the listener
closed with exit `0`.
diff --git a/examples/22-scaling-the-server/README.md b/examples/22-scaling-the-server/README.md
index f67c380..2c0df64 100644
--- a/examples/22-scaling-the-server/README.md
+++ b/examples/22-scaling-the-server/README.md
@@ -1,11 +1,11 @@
-# 22-scaling-the-server — chatterd v1
+# 22-scaling-the-server: chatterd v1
-chatterd is a peer-to-peer chat daemon we grow across chapters 21–24 and 27.
-ch21 introduced the length-prefixed frame protocol and a thread-per-connection
-server. ch22 is about **how a server holds many connections at once**: it keeps
-the threaded engine and adds a single-threaded **epoll** engine — one thread,
-nonblocking accept, and a per-connection read/write state machine with an
-outbound queue and real backpressure.
+chatterd is a peer-to-peer chat daemon that grows across several chapters in
+this book. The previous chapter introduced the length-prefixed frame protocol
+and a thread-per-connection server. Holding many connections at once is the
+focus here: the daemon keeps the threaded engine and adds a single-threaded
+**epoll** engine: one thread, nonblocking accept, and a per-connection
+read/write state machine with an outbound queue and real backpressure.
The daemon and its client are the same binary (`app`): `serve` is the daemon,
`send` / `listen` / `flood` are the client (`chatctl`) modes.
@@ -31,15 +31,15 @@ chatterd: stopped engine=epoll messages=2 peak_conns=50
Every MSG is delivered to **every** connected client, including the sender, so
a client confirms its message was accepted by seeing its own line echoed.
-## What the chapter is really about
+## Threaded vs. epoll
Two ways to service N connections from one process:
-- **`--engine threaded`** — one OS thread per connection blocks in `recv()`; a
+- **`--engine threaded`**: one OS thread per connection blocks in `recv()`; a
shared registry (mutex + per-socket write mutex) fans each message out. Simple
to reason about, one kernel thread per client. The accept loop `poll(2)`s the
listening socket alongside a `signalfd`, so a signal breaks it out cleanly.
-- **`--engine epoll`** — one thread owns an `epoll` set over the listener, a
+- **`--engine epoll`**: one thread owns an `epoll` set over the listener, a
`signalfd`, and every client socket. Accept is nonblocking (`accept4` in a
loop until `EAGAIN`); each connection is a state machine with an **inbound
buffer** (frames are reassembled across reads) and an **outbound queue**.
@@ -58,7 +58,7 @@ that the threaded engine does the same, and that both shut down cleanly with a
Go exposes the same `--engine threaded|epoll` flag, but **both** engines are
the same idiomatic code: one goroutine per connection reading, one writing, and
-a hub goroutine fanning out over channels. There is no hand-rolled epoll — the
+a hub goroutine fanning out over channels. There is no hand-rolled epoll: the
Go runtime's **network poller** already multiplexes every socket through
`epoll_wait(2)` and parks a *goroutine* (not an OS thread) on each blocked
Read/Write. The epoll the other two languages write by hand, Go's netpoller runs
@@ -67,8 +67,9 @@ identical across all three.
## Wire format
-One frame protocol, introduced in ch21 and reused unchanged by every later
-chatterd version and every language. All multi-byte fields are **big-endian**.
+One frame protocol, introduced earlier in the series and reused unchanged by
+every later chatterd version and every language. All multi-byte fields are
+**big-endian**.
```
byte: 0 1 2 3 4 5 6 ............. 6+LEN-1
@@ -92,22 +93,22 @@ Header is always 6 bytes; total frame length is `6 + LENGTH`.
| 0x02 | MSG | client → server | message text, UTF-8 |
| 0x03 | DELIVER | server → client | `nick + 0x00 (NUL) + text` |
| 0x04 | WELCOME | server → client | empty (sent once, on accept) |
-| 0x05 | PING | — | reserved (introduced in ch24; unused here) |
+| 0x05 | PING | n/a | reserved (introduced later; unused here) |
-This is the same frame every chatterd chapter and language speaks: a v0
-(ch21) client can chat through a v1 (this chapter's) or v3/v4 server and back
-because JOIN/MSG/DELIVER never change shape — later chapters only add types
-(WELCOME here, PING in ch24), never touch the 6-byte header or repurpose an
-existing type.
+This is the same frame every chatterd version and language speaks: a v0
+client can chat through a v1 (this chapter's) or v3/v4 server and back
+because JOIN/MSG/DELIVER never change shape; later versions only add types
+(WELCOME here, PING added later), never touch the 6-byte header or
+repurpose an existing type.
`DELIVER` carries the originating nickname, then a single NUL (`0x00`) byte,
-then the message text — split the payload on its first NUL to recover the two
+then the message text: split the payload on its first NUL to recover the two
fields (a nickname is never allowed to contain a NUL). WELCOME is sent the
-instant the server accepts a connection — receiving it is proof the connection
+instant the server accepts a connection; receiving it is proof the connection
is registered in the server's fan-out set, which is how `flood` knows all N
peers are present before it triggers the broadcast.
-Example — `alice` sends `hi` (`msg`), and the server delivers it:
+Example: `alice` sends `hi` (`msg`), and the server delivers it:
```
client -> server 43 48 01 01 00 05 61 6c 69 63 65 JOIN "alice"
@@ -144,7 +145,7 @@ Same CLI, stdout/stderr shapes, exit codes, and wire protocol, so one
| | epoll engine | threaded engine | shutdown |
|---|---|---|---|
| **C++23** | hand-rolled `epoll_create1`/`epoll_ctl`/`epoll_wait`; per-conn `EConn` with `inbuf`/`outbuf`, `EPOLLOUT` toggled on backpressure; RAII `Fd` owns every socket, `std::expected` for setup | `std::jthread` per connection, shared `Registry` (mutex + per-socket write mutex), `std::atomic` counters | `signalfd` in the loop / poll set; `SHUT_RDWR` wakes reader threads, jthreads join |
-| **Go 1.26** | netpoller (see note) — goroutine per conn + hub goroutine, channel fan-out with blocking sends for backpressure | *same code* as epoll; the flag only changes the printed label | `signal.Notify`, close listener, `quit`/`done` channels, atomics read after the hub drains |
+| **Go 1.26** | netpoller (see note): goroutine per conn + hub goroutine, channel fan-out with blocking sends for backpressure | *same code* as epoll; the flag only changes the printed label | `signal.Notify`, close listener, `quit`/`done` channels, atomics read after the hub drains |
| **Rust 2024** | `mio` `Poll`/`Token`/`Interest`; per-conn `EConn` with `inbuf`/`outbuf`, reregister `READABLE\|WRITABLE` on backpressure; `signalfd` registered via `SourceFd`; `OwnedFd`-backed `mio::net` sockets | `std::thread` per connection, `Arc` (mutex map + per-socket `Mutex` write clone), atomics | `nix::poll` over listener + `signalfd`; `TcpStream::shutdown` wakes readers, threads join |
Build/run: `./demo.sh [cpp|go|rust] build` and `./demo.sh run `.
diff --git a/examples/23-udp-and-peer-discovery/README.md b/examples/23-udp-and-peer-discovery/README.md
index f9e35b4..106a9b6 100644
--- a/examples/23-udp-and-peer-discovery/README.md
+++ b/examples/23-udp-and-peer-discovery/README.md
@@ -1,12 +1,13 @@
-# 23-udp-and-peer-discovery — chatterd v2
+# 23-udp-and-peer-discovery: chatterd v2
-chatterd grows peer discovery. Where chapters 21–22 built the TCP side (the
-canonical chatterd chat frame between two known endpoints), `chatterd
-discover` adds the missing half: finding peers you were never told about. It
-sends periodic **UDP multicast beacons**, listens for everyone else's, and the
-first time it hears a new peer it opens a TCP connection to that peer's
-advertised port and exchanges one chat frame each way — the same canonical
-wire frame every chatterd version speaks, reused here for JOIN/DELIVER.
+chatterd grows peer discovery. The TCP side built earlier in the book
+established the canonical chatterd chat frame between two known endpoints;
+`chatterd discover` adds the missing half: finding peers you were never told
+about. It sends periodic **UDP multicast beacons**, listens for everyone
+else's, and the first time it hears a new peer it opens a TCP connection to
+that peer's advertised port and exchanges one chat frame each way, the same
+canonical wire frame every chatterd version speaks, reused here for
+JOIN/DELIVER.
```
$ ./demo.sh go run discover --group 239.23.7.1 --port 51823 \
@@ -28,24 +29,24 @@ Each process announces on stderr (`chatterd: announcing as on
`--announce-ms` apart, then exits 0 after an 800 ms grace window so in-flight
exchanges finish.
-## What the chapter is really about
+## What the chapter is about
UDP multicast is the one-to-many primitive. A beacon sent once to a group
address (`239.0.0.0/8`, the administratively-scoped block) reaches every
member on the interface without the sender knowing who or how many they are.
Discovery is: **join the group, announce yourself, and learn from what you
-hear.** The three implementations exercise the same setsockopt dance —
+hear.** The three implementations exercise the same setsockopt dance:
- **`IP_ADD_MEMBERSHIP`** (`ip_mreq { imr_multiaddr = group, imr_interface =
- iface }`) — join the group on a specific interface. Without this the kernel
+ iface }`): join the group on a specific interface. Without this the kernel
drops group traffic before it reaches the socket.
-- **`IP_MULTICAST_IF`** — pin *outgoing* multicast to the same interface, so a
+- **`IP_MULTICAST_IF`**: pin *outgoing* multicast to the same interface, so a
multi-homed host sends the beacon where its peers are listening.
-- **`IP_MULTICAST_LOOP`** — deliver our own multicast to other sockets **on
+- **`IP_MULTICAST_LOOP`**: deliver our own multicast to other sockets **on
this host**. This is what makes the local two-process verify work over the
loopback interface; across two machines it is irrelevant (the packet is on
the wire either way).
-- **`IP_MULTICAST_TTL = 1`** — keep beacons on the local link; do not let a
+- **`IP_MULTICAST_TTL = 1`**: keep beacons on the local link; do not let a
router forward them.
The receive side dedupes by peer name, so "discovered peer …" prints exactly
@@ -58,11 +59,11 @@ loops group traffic **when the multicast interface is set to `127.0.0.1`**.
That is why the local verify passes `--iface 127.0.0.1`: both processes join
and send on the loopback address and the kernel loops each beacon to the
other. On the VMs the interface is the guest's `enp1s0` address and the
-beacons cross the `virbr0` bridge for real.
+beacons cross the `virbr0` bridge between the two guests.
## Wire formats (byte-exact, identical across all three languages)
-### 1. Discovery beacon — UDP datagram to `:`
+### 1. Discovery beacon: UDP datagram to `:`
An ASCII text datagram, no framing (UDP preserves message boundaries):
@@ -70,16 +71,16 @@ An ASCII text datagram, no framing (UDP preserves message boundaries):
CHATTERD1
```
-- `CHATTERD1` — literal magic / version tag (9 bytes).
+- `CHATTERD1`: literal magic / version tag (9 bytes).
- single ASCII spaces separate the four fields; `` contains no space.
-- `` — decimal, the TCP port this peer accepts chat frames on.
-- `` — dotted-quad the peer wants to be dialed at (its `--iface`).
+- ``: decimal, the TCP port this peer accepts chat frames on.
+- ``: dotted-quad the peer wants to be dialed at (its `--iface`).
A receiver splits on whitespace, requires exactly 4 fields with `tok[0] ==
"CHATTERD1"`, and ignores a beacon whose `` equals its own.
Captured on the peer guest's `enp1s0` during the two-host run (`tcpdump -X`),
-a beacon from systems-target — 35 payload bytes:
+a beacon from systems-target, 35 payload bytes:
```
IP 192.168.124.7.40516 > 239.23.7.1.51824: UDP, length 35
@@ -89,12 +90,12 @@ IP 192.168.124.7.40516 > 239.23.7.1.51824: UDP, length 35
= "CHATTERD1 target 9241 192.168.124.7"
```
-### 2. Chat frame — TCP, the canonical chatterd frame (reused, not this chapter's invention)
+### 2. Chat frame: TCP, the canonical chatterd frame (reused, not this chapter's invention)
-This is **not** a ch23-specific format — it is the one wire frame every
-chatterd version (ch21's thread-per-connection server, ch22's epoll engine,
-ch24's heartbeat client, ch27's async transport) reads and writes. ch23 just
-dials it peer-to-peer instead of client-to-server. The UDP beacon above is a
+This wire frame is shared across every chatterd version (the
+thread-per-connection server, the epoll engine, the heartbeat client, and the
+async transport covered elsewhere in the book); ch23 dials it peer-to-peer
+instead of client-to-server. The UDP beacon above is a
completely separate object; this frame only ever travels over the TCP
connection opened *after* discovery.
@@ -117,8 +118,8 @@ header. ch23's post-discovery exchange uses two of the five types, one frame
each way:
1. the discoverer dials `:` and sends a **JOIN** frame
- whose payload is its own name (``) — the same "announce on connect"
- JOIN chapters 21/22/24/27 use;
+ whose payload is its own name (``), the same "announce on connect"
+ JOIN pattern used by every other chatterd version;
2. the listener reads that frame, replies with a **DELIVER** frame whose
payload is `\0hello from ` (name, then a NUL byte, then the
greeting text), and closes;
@@ -126,10 +127,10 @@ each way:
byte, and prints `peer says: `.
Both peers discover each other independently, so each dials the other once and
-each prints one `peer … says:` line — the message crossed the network. Because
-the header is shared, a ch23 peer's JOIN/DELIVER exchange is wire-compatible
-with a ch21/ch22/ch24/ch27 chatterd speaking the same frame; the newer-only
-types (WELCOME, PING) simply never appear in this chapter's exchange.
+each prints one `peer … says:` line, confirming the message crossed the
+network. Because the header is shared, a ch23 peer's JOIN/DELIVER exchange is
+wire-compatible with any other chatterd version speaking the same frame; the
+newer-only types (WELCOME, PING) never appear in this chapter's exchange.
## The three implementations
@@ -159,7 +160,7 @@ Flags: `--group ` and `--port ` and `--name ` are required;
## Verification
`verify.lua` (mode `vm-peer`) runs the **local** two-process assertion on
-loopback multicast — it clears `TARGET` so `demo.sh run` executes on the host,
+loopback multicast: it clears `TARGET` so `demo.sh run` executes on the host,
starts `alice` and `bob` with distinct names and TCP ports, and asserts each
discovers the other exactly once, exchanges the chat frame, and exits 0, plus
the usage/exit-code shapes. Distinct ports per language keep back-to-back
diff --git a/examples/24-time-timers-deadlines/README.md b/examples/24-time-timers-deadlines/README.md
index 221566f..a4d92e3 100644
--- a/examples/24-time-timers-deadlines/README.md
+++ b/examples/24-time-timers-deadlines/README.md
@@ -1,7 +1,7 @@
-# 24-time-timers-deadlines — chatterd v3, heartbeats + deadlines
+# 24-time-timers-deadlines: chatterd v3, heartbeats + deadlines
-Chapter 24 grows `chatterd` — the peer-to-peer chat daemon introduced in
-chapter 21 — into a version that survives a peer going away. The lesson is
+This chapter grows `chatterd` (the peer-to-peer chat daemon introduced in
+the TCP sockets chapter) into a version that survives a peer going away. The lesson is
*time*: how to keep a connection alive with a periodic heartbeat, how to
declare a peer dead by measuring **silence on a monotonic clock**, and how to
come back with a **jittered exponential backoff** instead of a hot reconnect
@@ -32,8 +32,8 @@ opts: --heartbeat-ms H (default 1000) PING interval
`res` is `clock_getres(CLOCK_MONOTONIC)`; the two `actual` fields are the
measured wall-time of a 1 ms `nanosleep(2)` and a 1 ms one-shot `timerfd`,
each timed on a **monotonic/steady clock** (never `CLOCK_REALTIME`). On a
- typical host both land near 1050–1100 µs — the overshoot is the scheduler's
- wake-up latency, which is exactly the point.
+ typical host both land near 1050–1100 µs: the overshoot is the scheduler's
+ wake-up latency, which is the point.
- **`listen`** binds, prints `chatterd: listening on :` (a
`:0` port is reported as the actual ephemeral port), and accepts a peer.
@@ -49,14 +49,14 @@ opts: --heartbeat-ms H (default 1000) PING interval
received frame (JOIN/MSG/PING) is "traffic" and resets a
`--timeout-ms` deadline. When the deadline fires with no traffic the peer
is dropped: `chatterd: peer timed out`. A closed socket (peer
- killed) is handled the same way — reads stop, but it is the *deadline*, on
+ killed) is handled the same way: reads stop, but it is the *deadline*, on
the monotonic clock, that declares death.
- After a drop the **connector** reconnects with equal-jitter exponential
- backoff — `delay = B/2 + rand(0..B/2)`, with `B` doubling each failed
+ backoff (`delay = B/2 + rand(0..B/2)`, with `B` doubling each failed
attempt up to `--max-backoff-ms` and reset to the base on a fresh
- connection — printing `chatterd: reconnecting to in ms` before
- each wait. The **listener** simply returns to `accept()` to welcome a
+ connection), printing `chatterd: reconnecting to in ms` before
+ each wait. The **listener** returns to `accept()` to welcome a
reconnecting peer.
- **SIGINT / SIGTERM** at any point (linked, mid-backoff, or waiting to
@@ -66,11 +66,13 @@ opts: --heartbeat-ms H (default 1000) PING interval
## Wire format (identical across languages and every chatterd version)
-This is **the** chatterd chat frame: introduced in chapter 21, and extended
-in later chapters only by adding new `TYPE` values — the header itself never
-changes, so every chatterd version (thread-per-connection in ch21, epoll in
-ch22, the UDP-discovered peers of ch23, this chapter's heartbeats, the async
-runtimes of ch27) reads and writes exactly these bytes. All multi-byte
+This is **the** chatterd chat frame: introduced earlier in the series, and
+extended in later chapters only by adding new `TYPE` values. The header
+itself never changes, so every chatterd version (thread-per-connection in
+the TCP sockets chapter, epoll in the server-scaling chapter, the
+UDP-discovered peers of the peer-discovery chapter, this chapter's
+heartbeats, the async runtimes of a later chapter) reads and writes exactly
+these bytes. All multi-byte
integers are big-endian. The header is a fixed 6 bytes:
```
@@ -96,13 +98,14 @@ Framing rules, byte-exact and version-stable:
- `MAGIC`/`VERSION` mismatch is a protocol error and drops the connection.
- `PING` is the frame type added in **v3** (this chapter) for heartbeating.
`DELIVER` and `WELCOME` belong to the server-broadcast chatterd versions
- (ch22, ch27) — this chapter's peer-to-peer `listen`/`connect` model never
- emits them, but since the header is shared, a peer that ever receives one
- (or any `0x06..0xFF` value) treats it as generic liveness traffic and skips
- its `LENGTH` bytes rather than erroring. `JOIN` (the peer-name handshake)
- and `MSG` (chat text) are unchanged since ch21, so a v3 peer here still
+ (the server-scaling and async-runtimes chapters); this chapter's
+ peer-to-peer `listen`/`connect` model never emits them, but since the
+ header is shared, a peer that ever receives one (or any `0x06..0xFF`
+ value) treats it as generic liveness traffic and skips its `LENGTH` bytes
+ rather than erroring. `JOIN` (the peer-name handshake) and `MSG` (chat
+ text) are unchanged since the earliest version, so a v3 peer here still
links with older or newer chatterd peers for everything except
- heartbeating — dropping a silent peer is the only thing PING adds.
+ heartbeating: dropping a silent peer is the only thing PING adds.
Example: a JOIN announcing the name `A` is the 7 bytes
`43 48 01 01 00 01 41`; a PING is the 6 bytes `43 48 01 05 00 00`.
@@ -112,24 +115,24 @@ Example: a JOIN announcing the name `A` is the 7 bytes
The protocol and the observable lines are identical; how each language builds
the heartbeat/deadline loop is the lesson.
-- **C++ — `timerfd_create(CLOCK_MONOTONIC)` + `poll(2)`.** A periodic timerfd
+- **C++: `timerfd_create(CLOCK_MONOTONIC)` + `poll(2)`.** A periodic timerfd
is the heartbeat; a `std::chrono::steady_clock` deadline is peer liveness
(the poll timeout is the time left until it). The peer socket, the heartbeat
timerfd, and a `signalfd(2)` shutdown doorbell share one `poll` loop. Every
fd is owned by a RAII `UniqueFd`; sends use `MSG_NOSIGNAL`, and errors flow
through `std::expected`.
-- **Go — `time.Ticker` + `time.Timer` + `context`.** A `Ticker` fires the
+- **Go: `time.Ticker` + `time.Timer` + `context`.** A `Ticker` fires the
heartbeat, a `Timer` is the deadline (`Reset` on every frame), and the
context from `signal.NotifyContext` is shutdown. A per-connection reader
goroutine turns the socket into a channel of frames, so the session is one
`select` over ticker / deadline / frames / `ctx.Done()`. `time.Now` carries
a monotonic reading, so `time.Since` never sees a wall-clock step.
-- **Rust — `nix` timerfd + `std::time::Instant` + `poll`.** A periodic
+- **Rust: `nix` timerfd + `std::time::Instant` + `poll`.** A periodic
`nix::sys::timerfd::TimerFd` is the heartbeat, an `Instant` deadline is peer
- liveness, and a `nix` `SignalFd` is shutdown — all three in one
+ liveness, and a `nix` `SignalFd` is shutdown: all three in one
`nix::poll::poll` loop over `std` `TcpStream`/`TcpListener` fds (`OwnedFd`
- ownership throughout). Errors flow through `Result`/`?`. No tokio here —
- async is chapter 27.
+ ownership throughout). Errors flow through `Result`/`?`. No tokio here;
+ async is covered in a later chapter.
## Layout
@@ -144,7 +147,7 @@ the heartbeat/deadline loop is the lesson.
Each language directory has a `demo.sh` with the identical interface used
book-wide: `./demo.sh build`, `./demo.sh run [args]`, and a bare `./demo.sh`
-that builds then runs. With `TARGET` set, `run` deploys to that lab VM — but
+that builds then runs. With `TARGET` set, `run` deploys to that lab VM, but
this chapter runs purely on the host (loopback), so `TARGET` is unused here.
Try it by hand:
diff --git a/examples/25-shared-state-and-the-futex/README.md b/examples/25-shared-state-and-the-futex/README.md
index 5eb607d..e644cb6 100644
--- a/examples/25-shared-state-and-the-futex/README.md
+++ b/examples/25-shared-state-and-the-futex/README.md
@@ -1,7 +1,7 @@
# 25-shared-state-and-the-futex
Chapter 25's example: `workq`, a bounded **MPMC job queue + worker pool**
-implemented three times — C++23, Go, and Rust — with each language's native
+implemented three times, C++23, Go, and Rust, with each language's native
shared-state toolkit, but one observable contract so a single `verify.lua`
covers all three. The queue's blocking is exactly the mutex/condvar dance the
kernel implements with **futexes**; `strace`-ing any of these binaries shows
@@ -24,7 +24,7 @@ workq: produced= consumed= checksum= ms=
- `produced` / `consumed` are exact counts (both equal `N` on the correct path).
- `checksum` is a 16-digit zero-padded lowercase hex `u64`.
-- `ms` is wall-clock milliseconds (timing — the only non-deterministic field).
+- `ms` is wall-clock milliseconds (timing, the only non-deterministic field).
Defaults: `--seed 0x0123456789ABCDEF`, `--cap 256`. `--producers`,
`--consumers`, and `--items` are required.
@@ -37,7 +37,7 @@ a flag with no value → `workq: --producers needs a value`; a non-integer value
## The value contract (identical across languages and runs)
Each item index `i ∈ [0, N)` has a payload that is a **pure function** of
-`(seed, i)` — a splitmix64 finalizer over `seed + (i+1)·GOLDEN`, all in wrapping
+`(seed, i)`: a splitmix64 finalizer over `seed + (i+1)·GOLDEN`, all in wrapping
`u64` arithmetic:
```
@@ -55,7 +55,7 @@ Producer `p` owns the indices `{ i : i mod P == p }`, so the **set** of items is
`{0..N-1}` for any `P`. The checksum folds payloads with **XOR**, which is
commutative and associative, so the result is independent of how items are
split among producers or the order consumers drain them. For a fixed
-`(seed, N)` the checksum is therefore one fixed constant — byte-identical in
+`(seed, N)` the checksum is therefore one fixed constant, byte-identical in
C++, Go, and Rust:
| seed | N | checksum |
@@ -82,16 +82,16 @@ workq: produced=100000 consumed=100000 checksum=42b3746c6cee7465 ms=62
|---|---|---|
| **C++23** | `std::mutex` + two `std::condition_variable`s (`not_full` / `not_empty`) guarding a `std::deque`; `std::jthread` producers/consumers, `std::atomic` produced counter | per-consumer local fold, XOR-combined after join |
| **Go** | a buffered `chan uint64` (capacity `K`) as the blocking queue; a closer goroutine `sync.WaitGroup.Wait()`s the producers then `close()`s the channel, ending every consumer's `range` | per-consumer local fold, XOR-combined after the consumer `WaitGroup` |
-| **Rust** | `Mutex>` + two `Condvar`s inside `std::thread::scope`; no `Arc` — the scope lets the threads borrow the queue directly | per-consumer `(count, sum)` returned from `ScopedJoinHandle::join`, XOR-combined |
+| **Rust** | `Mutex>` + two `Condvar`s inside `std::thread::scope`; no `Arc`, the scope lets the threads borrow the queue directly | per-consumer `(count, sum)` returned from `ScopedJoinHandle::join`, XOR-combined |
-None of these needs an atomic on the hot path except the produced counter — the
+None of these needs an atomic on the hot path except the produced counter: the
mutex/condvar (or channel) already establishes the happens-before edges.
-## The `--buggy` demo (a real data race — chapter material, not verified)
+## The `--buggy` demo (a data race, chapter material, not verified)
`--buggy` keeps the same correct, synchronized queue but routes **every**
consumer into a single **unsynchronized** shared counter + checksum. That is a
-genuine data race; `verify.lua` never runs it, but the chapter does:
+data race; `verify.lua` never runs it, but the chapter does:
- **Go** builds with `-race` (see `go/demo.sh`), so `--buggy` prints
`WARNING: DATA RACE` and ends with `Found N data race(s)` and a nonzero exit.
@@ -99,7 +99,7 @@ genuine data race; `verify.lua` never runs it, but the chapter does:
- **C++** has a ThreadSanitizer preset: `cmake --preset tsan && cmake --build
--preset tsan`, then `./build/tsan/app ... --buggy` reports
`WARNING: ThreadSanitizer: data race` at the two shared-write lines.
-- **Rust** won't let you write this in safe code at all — the buggy path needs
+- **Rust** won't let you write this in safe code at all: the buggy path needs
an `unsafe` `*mut` shared across threads. Run it and the borrow checker's
point is made empirically: `consumed` and `checksum` come out different every
time (`consumed=97282`, `95773`, …), never the correct `100000` /
@@ -120,9 +120,9 @@ genuine data race; `verify.lua` never runs it, but the chapter does:
Every language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
@@ -135,18 +135,18 @@ run …`; bare `./demo.sh` (or `all`) builds and runs all three.
`verify.lua` runs from this directory with `LSP_LANG` set to one language at a
time and asserts, per language (25 checks each):
-1. the error contract — no args / unknown flag / missing required flag / bad
+1. the error contract: no args / unknown flag / missing required flag / bad
integer / value-less flag all print the usage line and exit 2;
2. a fixed run (`P=4 C=4 N=100000`, default seed) reports
`produced=100000 consumed=100000` and the shared checksum constant
`42b3746c6cee7465`;
-3. determinism — three more shapes with different `P`, `C`, and `--cap` over the
+3. determinism: three more shapes with different `P`, `C`, and `--cap` over the
same `(N, seed)` all yield that same constant with `produced==consumed==N`;
4. seed `42` (`N=50000`) yields its own shared constant `fceb5608a4b73dc0`, and
`N=0` drains to `produced=0 consumed=0 checksum=0000000000000000`.
-The `--buggy` race is a sanitizer/`-race` chapter demo and is deliberately
-outside the passing verify.
+The `--buggy` race is a sanitizer/`-race` chapter demo, kept outside the
+passing verify.
Verified on the Fedora 44 reference host (kernel 7.1, GCC 16.1.1,
go1.26.5, rustc 1.97.1): all three languages PASS 25/25, Go under `-race`.
diff --git a/examples/26-atomics-and-lock-free/README.md b/examples/26-atomics-and-lock-free/README.md
index 0d3ce37..518c245 100644
--- a/examples/26-atomics-and-lock-free/README.md
+++ b/examples/26-atomics-and-lock-free/README.md
@@ -1,7 +1,7 @@
# 26-atomics-and-lock-free
-Chapter 26's example: `spscring`, a single-producer/single-consumer lock-free
-ring buffer implemented three times — C++23, Go, and Rust — each with that
+This chapter's example: `spscring`, a single-producer/single-consumer lock-free
+ring buffer implemented three times, in C++23, Go, and Rust, each with that
language's atomics, but one shared observable contract so a single `verify.lua`
covers all three.
@@ -15,7 +15,7 @@ A **producer** thread pushes `N` `u64` values (`0, 1, ..., N-1`) through a
bounded ring of `K` slots; the **consumer** (the main thread) pops each value
and sums it. There is no mutex and no lock: the ring is coordinated by two
`u64` atomic counters, `head` (advanced by the consumer) and `tail` (advanced
-by the producer), using acquire/release ordering — Lamport's classic SPSC
+by the producer), using acquire/release ordering: Lamport's classic SPSC
queue. When the producer has finished all `N` pushes and the consumer all `N`
pops, the program prints exactly one line:
@@ -23,15 +23,15 @@ pops, the program prints exactly one line:
spscring: items=1000000 sum=499999500000 throughput_mops=37.29 pad=off
```
-- `items=` — echoes the requested count.
-- `sum=` — the consumer's running total. Because the values are `0..N-1`,
+- `items=`: echoes the requested count.
+- `sum=`: the consumer's running total. Because the values are `0..N-1`,
a correct run **always** yields `s = N·(N-1)/2`. Any dropped, duplicated, or
torn hand-off changes this number, so the sum is a concurrency oracle.
-- `throughput_mops=` — millions of pushes per second, `N / wall_seconds / 1e6`,
+- `throughput_mops=`: millions of pushes per second, `N / wall_seconds / 1e6`,
printed with two decimals. Its magnitude is machine- and run-dependent; the
book uses it only to *narrate* the false-sharing effect, and `verify.lua`
only asserts that it parses as a positive number.
-- `pad=` — echoes the `--pad` mode.
+- `pad=`: echoes the `--pad` mode.
The three binaries produce the same line shape and the same `items`/`sum`/`pad`
fields for the same arguments.
@@ -39,16 +39,16 @@ fields for the same arguments.
**`--pad on|off`** toggles cache-line padding between `head` and `tail`
(default `off`):
-- `off` — the two counters sit adjacent in one 64-byte cache line. The
+- `off`: the two counters sit adjacent in one 64-byte cache line. The
producer's `store(tail)` and the consumer's `store(head)` then invalidate
- each other's copy of the shared line on every hand-off — **false sharing** —
+ each other's copy of the shared line on every hand-off (**false sharing**),
even though neither thread touches the other's counter.
-- `on` — each counter is `alignas(64)` / `#[repr(align(64))]` / padded onto its
+- `on`: each counter is `alignas(64)` / `#[repr(align(64))]` / padded onto its
own cache line, so the two cores stop fighting over one line.
Both modes compute the same, correct sum; the throughput difference between
them is the chapter's whole point, not a hard assertion (on a lightly loaded
-box a single 1M-item run is noisy — the book measures the gap with repeats).
+box a single 1M-item run is noisy; the book measures the gap with repeats).
## The ring protocol (identical across languages)
@@ -81,13 +81,13 @@ push(v): pop() -> v:
| | index atomics | memory ordering | padding knob |
|---|---|---|---|
| **C++23** | `std::atomic` head/tail in a `Control` struct; producer is a `std::jthread`, consumer is `main` | explicit `memory_order_acquire` on the loads, `memory_order_release` on the stores; the plain `std::vector` slots ride the release/acquire edge | `Control` gives head and tail each `alignas(64)`; `Control` packs them adjacent |
-| **Go** | `atomic.Uint64` head/tail; producer goroutine, consumer on `main`, joined over a `chan struct{}` | **no per-op order argument** — Go's `sync/atomic` is sequentially consistent, and the memory model's "an atomic write is synchronized-before the read that observes it" *is* the acquire/release edge, just not relaxable | `ctrlPadded` inserts `[56]byte` after each counter; `ctrlPacked` leaves them adjacent |
-| **Rust** | `AtomicU64` head/tail shared across a `std::thread::scope`; slots are `Vec` too | explicit `Ordering::{Acquire, Release}` on head/tail, `Ordering::Relaxed` on the slots — so the data race is ruled out by the type system, no `unsafe` | `CtrlPadded` wraps each counter in `#[repr(align(64))]`; `CtrlPacked` leaves them adjacent |
+| **Go** | `atomic.Uint64` head/tail; producer goroutine, consumer on `main`, joined over a `chan struct{}` | **no per-op order argument**: Go's `sync/atomic` is sequentially consistent, and the memory model's "an atomic write is synchronized-before the read that observes it" *is* the acquire/release edge, just not relaxable | `ctrlPadded` inserts `[56]byte` after each counter; `ctrlPacked` leaves them adjacent |
+| **Rust** | `AtomicU64` head/tail shared across a `std::thread::scope`; slots are `Vec` too | explicit `Ordering::{Acquire, Release}` on head/tail, `Ordering::Relaxed` on the slots, so the data race is ruled out by the type system, no `unsafe` | `CtrlPadded` wraps each counter in `#[repr(align(64))]`; `CtrlPacked` leaves them adjacent |
**What Go gives you (and doesn't):** C++ and Rust let you name the exact
ordering per operation (`acquire`, `release`, `relaxed`) and could, in
principle, relax the slot accesses further. Go's `sync/atomic` exposes only
-sequentially-consistent `Load`/`Store`/`Add` — you cannot ask for a weaker
+sequentially-consistent `Load`/`Store`/`Add`: you cannot ask for a weaker
order, but you also cannot get it wrong: every atomic access already carries
the happens-before edge this queue needs. The Go build runs under `-race`
(see `go/demo.sh`); the detector understands the atomic edge and stays silent
@@ -109,14 +109,14 @@ race-free rather than merely lucky.
Every language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
(e.g. `./demo.sh run --capacity 1024 --items 1000000 --pad on`)
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
-This is a host-only example — pure threads and atomics, no sockets, no VM.
+This is a host-only example: pure threads and atomics, no sockets, no VM.
## Verification
@@ -124,11 +124,11 @@ This is a host-only example — pure threads and atomics, no sockets, no VM.
time and asserts, per language:
1. `--capacity 1024 --items 1000000 --pad off` exits 0 and prints
- `items=1000000 sum=499999500000 throughput_mops= pad=off` — the sum
+ `items=1000000 sum=499999500000 throughput_mops= pad=off`; the sum
is the exact triangular number `N·(N-1)/2`, so every one of the million
hand-offs delivered its value once;
2. the same with `--pad on` yields the identical sum with `pad=on`;
-3. `--capacity 1 --items 1000` (a one-slot ring — maximum head/tail
+3. `--capacity 1 --items 1000` (a one-slot ring, maximum head/tail
contention) still sums to exactly `499500`;
4. the `throughput_mops` field parses as a positive number;
5. no arguments prints `usage: spscring --capacity K --items N [--pad on|off]`
diff --git a/examples/27-async-runtimes-and-coroutines/README.md b/examples/27-async-runtimes-and-coroutines/README.md
index fa53e44..f9dc039 100644
--- a/examples/27-async-runtimes-and-coroutines/README.md
+++ b/examples/27-async-runtimes-and-coroutines/README.md
@@ -1,16 +1,16 @@
-# 27 — async runtimes and coroutines (chatterd v4)
+# 27: async runtimes and coroutines (chatterd v4)
`chatterd` is the book's recurring peer-to-peer chat daemon. This chapter is the
**async rewrite**: the *same* server, with its connection handling rebuilt on
-each language's async model. The observable behaviour is unchanged — a message
-from one client is broadcast to every other connected client — so a single
+each language's async model. The observable behaviour is unchanged: a message
+from one client is broadcast to every other connected client, so a single
`verify.lua` covers all three languages.
| language | `--engine async` is… |
|----------|------------------------------------------------------------------|
| **C++** | hand-rolled **C++20 coroutines** over an epoll reactor: each connection is a coroutine that `co_await`s socket readability. The `task`/awaitable machinery lives in `cpp/src/main.cpp`; no external async library is linked. |
| **Rust** | **tokio**: an `async fn` per connection on a `tokio::net::TcpListener`, spawned as tasks, fanning out over per-client `mpsc` channels. |
-| **Go** | **unchanged from v1** — goroutines already *are* the async model. `--engine async` selects the same goroutine-per-connection hub. That is the lesson: Go has no separate coroutine layer to add. |
+| **Go** | **unchanged from v1**: goroutines already *are* the async model. `--engine async` selects the same goroutine-per-connection hub. That is the lesson: Go has no separate coroutine layer to add. |
Every version also keeps the prior **`--engine thread`** engine working
(one OS thread per client), so you can compare the two side by side and confirm
@@ -18,10 +18,10 @@ they are byte-for-byte identical on the wire.
## The wire format (canonical chatterd chat frame)
-This is the **same frame every chatterd chapter uses** — introduced in ch21
-and extended only additively (by adding new `type` values) ever since; the
-header never changes shape. Every message on a connection, in both
-directions, is a single frame:
+This is the **same frame every chatterd version uses**: introduced earlier
+in the series and extended only additively (by adding new `type` values)
+ever since; the header never changes shape. Every message on a connection,
+in both directions, is a single frame:
```
byte: 0 1 2 3 4 5 6 6+L-1
@@ -31,28 +31,29 @@ directions, is a single frame:
+----+----+----+----+----+----+=========================+
```
-* `magic` — 2 bytes, always `0x43 0x48` (ASCII `"CH"`).
-* `version` — 1 byte, `0x01`.
-* `type` — 1 byte: `JOIN=1`, `MSG=2`, `DELIVER=3`, `WELCOME=4`, `PING=5`.
-* `length` (`L`) — a 16-bit **big-endian** unsigned integer: the number of
+* `magic`: 2 bytes, always `0x43 0x48` (ASCII `"CH"`).
+* `version`: 1 byte, `0x01`.
+* `type`: 1 byte: `JOIN=1`, `MSG=2`, `DELIVER=3`, `WELCOME=4`, `PING=5`.
+* `length` (`L`): a 16-bit **big-endian** unsigned integer: the number of
bytes in `payload` (0‥65535).
-* `payload` — `L` bytes, UTF-8, type-specific (see below).
+* `payload`: `L` bytes, UTF-8, type-specific (see below).
This chapter's engines (thread and async, in all three languages) use only
-three of the five types — the same three ch21 introduced:
+three of the five types: the same three introduced earlier in the series:
-* **JOIN** — the **first** frame a client sends after connecting; payload is
+* **JOIN**: the **first** frame a client sends after connecting; payload is
the client's nick (e.g. `alice`).
-* **MSG** — every **later** frame the client sends; payload is message text.
-* **DELIVER** — for a MSG with payload `T` from the client whose nick is `N`,
+* **MSG**: every **later** frame the client sends; payload is message text.
+* **DELIVER**: for a MSG with payload `T` from the client whose nick is `N`,
the server sends every **other** connected client a DELIVER frame whose
payload is exactly `N + 0x00 (NUL) + T`. The sender never receives its own
message.
-`WELCOME` and `PING` are other chapters' additions to this same frame (ch22's
-welcome/roster message, ch24's heartbeat) — this chapter never sends them, and
-because the header is unchanged, a ch21-era client that doesn't know those
-types still exchanges JOIN/MSG/DELIVER correctly with this chapter's server.
+`WELCOME` and `PING` are other chapters' additions to this same frame (one
+chapter's welcome/roster message, another's heartbeat): this chapter never
+sends them, and because the header is unchanged, an earlier-version client
+that doesn't know those types still exchanges JOIN/MSG/DELIVER correctly
+with this chapter's server.
### Byte-exact examples
@@ -63,7 +64,7 @@ DELIVER "alice\0hi" 43 48 01 03 00 08 61 6c 69 63 65 00 68 69
```
That last line is a real capture: with `alice` and `bob` both connected,
-`alice` sending `hi` puts these fourteen bytes on `bob`'s socket — the 6-byte
+`alice` sending `hi` puts these fourteen bytes on `bob`'s socket: the 6-byte
header (`43 48` magic, `01` version, `03` type DELIVER, `00 08` length) then
the 8-byte payload `alice\0hi` (nick, NUL, text). The framing and the
`N NUL T` payload are identical across C++, Go, and Rust, and across the
@@ -116,9 +117,9 @@ background, waits for the port, runs the 20-client `loadtest`, and asserts the
observable behaviour for **both** engines:
* `chatterd: listening on 127.0.0.1:PORT engine=` announced,
-* `loadtest: delivered 20/20` — the broadcast reached all 20 clients,
+* `loadtest: delivered 20/20`: the broadcast reached all 20 clients,
* the loadtest exits `0`,
-* `SIGTERM` produces `chatterd: shutdown` and a `0` exit — a clean shutdown,
+* `SIGTERM` produces `chatterd: shutdown` and a `0` exit: a clean shutdown,
plus the CLI contract (missing subcommand → exit 2, unknown engine → exit 2).
The same assertions pass identically for C++, Go, and Rust.
@@ -139,12 +140,12 @@ The same assertions pass identically for C++, Go, and Rust.
`cpp/src/main.cpp` implements the minimum coroutine runtime the async engine
needs, and nothing more:
-* **`Detached`** — a fire-and-forget coroutine (eager start, self-destroying at
+* **`Detached`**: a fire-and-forget coroutine (eager start, self-destroying at
`final_suspend`). The acceptor and each per-connection handler are `Detached`.
-* **`Task`** — a lazy, awaitable coroutine that returns a value and uses
+* **`Task`**: a lazy, awaitable coroutine that returns a value and uses
symmetric transfer to resume its awaiter on completion. `fill()` (one
non-blocking `recv`, suspending on `EAGAIN`) is a `Task`.
-* **`Reactor`** — an epoll loop mapping a waited-on fd to the
+* **`Reactor`**: an epoll loop mapping a waited-on fd to the
`std::coroutine_handle` parked on its readability (`EPOLLONESHOT`, re-armed on
each `co_await`). A `co_await ReadAwaitable{reactor, fd}` suspends the current
coroutine until `fd` is readable.
diff --git a/examples/28-gdb-and-remote-debugging/README.md b/examples/28-gdb-and-remote-debugging/README.md
index 63a3f59..901849e 100644
--- a/examples/28-gdb-and-remote-debugging/README.md
+++ b/examples/28-gdb-and-remote-debugging/README.md
@@ -1,11 +1,11 @@
-# 28-gdb-and-remote-debugging — bugfarm scenario 1: a crashing pmon
+# 28-gdb-and-remote-debugging: bugfarm scenario 1, a crashing pmon
-Chapter 28 is the first of the "bugfarm" examples: `app` is deliberately
-dual-use. `app run` is a normal, well-behaved program. `app crash` is a
-debugging TARGET — it walks the identical three/four-frame call chain but
+This chapter is the first of the "bugfarm" examples: `app` is dual-use by
+design. `app run` is a normal, well-behaved program. `app crash` is a
+debugging TARGET: it walks the identical three/four-frame call chain but
feeds it a value that was never filled in, and goes straight into the bug
-class this chapter is about: reading through a pointer/reference/`Option`
-that a concurrent-looking event handler assumed — wrongly — was already
+class central to this chapter: reading through a pointer/reference/`Option`
+that a concurrent-looking event handler assumed (wrongly) was already
populated.
```
@@ -24,10 +24,10 @@ main -> run_supervisor -> handle_child -> deref
`run_supervisor` fakes a supervisor that just reaped child pid 4242. In
`run` mode the reaper filled in the child's exit-status slot before
`handle_child` looks at it. In `crash` mode, `handle_child` runs as if it
-fired *before* the reaper got there — the slot was never populated — which
-is exactly the shape of race a real signal-driven supervisor (see chapter
-12's `pmon`) can hit if a `SIGCHLD` notification and the code that consumes
-it get out of step.
+fired *before* the reaper got there (the slot was never populated), which
+is exactly the shape of race a real signal-driven supervisor (see the
+signal-handling chapter's `pmon`) can hit if a `SIGCHLD` notification and
+the code that consumes it get out of step.
- `app run` prints:
```
@@ -37,7 +37,7 @@ it get out of step.
```
and exits 0.
- `app crash` prints only `pmon: supervisor starting`, then hits the bug.
- **What happens next is different in every language — that difference is
+ **What happens next is different in every language; that difference is
the point of this chapter:**
| | mechanism | exit | core? | name the frame with |
@@ -52,25 +52,25 @@ it get out of step.
### Why C++ gets a core and Go/Rust don't
C++ has no runtime standing between your bug and the kernel: a null-pointer
-read simply generates a hardware page fault, the kernel delivers `SIGSEGV`,
+read generates a hardware page fault, the kernel delivers `SIGSEGV`,
and the default disposition for an unhandled `SIGSEGV` is "dump core, then
terminate." Go and Rust both interpose a runtime between your code and the
raw fault. Go's runtime installs its own `SIGSEGV` handler, recognizes the
fault came from ordinary code (not from inside the runtime itself), and
-converts it into a normal — if unrecovered — panic; there is no
+converts it into a normal (if unrecovered) panic; there is no
`sigaction(SA_RESETHAND)` back to the default disposition, so no core.
Rust's `.unwrap()` calls `panic!` directly: no signal is even involved,
just the ordinary unwind-and-abort (here, unwind-and-`exit(101)`) machinery.
`gdb` still attaches and runs both binaries fine, but there is no signal to
-catch — asking gdb for a backtrace after the fact gets you nothing useful,
+catch: asking gdb for a backtrace after the fact gets you nothing useful,
which is why the Go/Rust checks below read the runtime's own trace instead.
-One more thing worth noticing once you're inside gdb on the C++ build: the
-`ShutdownGuard` RAII type in `cpp/src/main.cpp`, whose destructor prints the
-"exiting cleanly" line, never fires on the crash path. RAII buys you
-cleanup on every C++-level exit (`return`, an exception, `std::exit` via a
-`terminate` handler) — it buys you nothing against a hardware fault, because
-a signal doesn't unwind the stack. Frame `#0` in the release backtrace below
+Inside gdb on the C++ build, the `ShutdownGuard` RAII type in
+`cpp/src/main.cpp`, whose destructor prints the "exiting cleanly" line,
+never fires on the crash path. RAII buys you cleanup on every C++-level
+exit (`return`, an exception, `std::exit` via a `terminate` handler); it
+buys you nothing against a hardware fault, because a signal doesn't unwind
+the stack. Frame `#0` in the release backtrace below
even lands a level *inside* `std::println`'s own formatting machinery,
rather than exactly on `*child.exit_slot`: at `-O2`, forming the reference
argument doesn't fault, only reading through it during formatting does.
@@ -79,8 +79,8 @@ argument doesn't fault, only reading through it during formatting does.
Each language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (builds first if needed)
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (builds first if needed)
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
@@ -89,7 +89,7 @@ All three builds keep debug info in an otherwise optimized binary: the C++
emits DWARF, and Rust's `Cargo.toml` sets `[profile.release] debug = true`.
Every frame function (`deref`, `handle_child`, `run_supervisor`) is marked
noinline (`[[gnu::noinline]]` / `//go:noinline` / `#[inline(never)]`) so
-optimization can't fold the call chain away — Rust's `handle_child` also
+optimization can't fold the call chain away: Rust's `handle_child` also
needs a `std::hint::black_box(())` after the call, or the optimizer turns it
into a tail call and erases its own frame from the backtrace.
@@ -155,17 +155,17 @@ RUST_BACKTRACE=1 ./target/release/app crash
`verify.lua` (run per language with `LSP_LANG` set) asserts:
- `run` is identical across languages: exit 0, the three status lines.
-- bare / unknown subcommand: exit 2, usage block — identical across languages.
+- bare / unknown subcommand: exit 2, usage block, identical across languages.
- `crash`, per language:
- - **cpp** — exit 139 (SIGSEGV); the RAII cleanup line never appears; under
+ - **cpp**: exit 139 (SIGSEGV); the RAII cleanup line never appears; under
`ulimit -c unlimited` in a scratch temp dir, `coredumpctl` finds and
extracts a core for the crashed pid; a scripted
`gdb --batch -ex run -ex bt` names `deref`, `handle_child`, and
`run_supervisor` in the backtrace.
- - **go** — exit 2 (not 139, no core); the panic message names the
+ - **go**: exit 2 (not 139, no core); the panic message names the
standard nil-pointer-dereference text; the goroutine trace names
`main.deref`, `main.handleChild`, `main.runSupervisor`.
- - **rust** — exit 101 (not 139, no core) either way; with
+ - **rust**: exit 101 (not 139, no core) either way; with
`RUST_BACKTRACE=1`, the backtrace names `app::deref`,
`app::handle_child`, `app::run_supervisor`; without it, those names are
absent (the backtrace is opt-in) but the exit code is unchanged.
diff --git a/examples/29-valgrind-sanitizers-miri/README.md b/examples/29-valgrind-sanitizers-miri/README.md
index 2290d1a..9fa3468 100644
--- a/examples/29-valgrind-sanitizers-miri/README.md
+++ b/examples/29-valgrind-sanitizers-miri/README.md
@@ -1,6 +1,6 @@
-# 29-valgrind-sanitizers-miri — bugfarm
+# 29-valgrind-sanitizers-miri: bugfarm
-Chapter 29 is a menu of five seeded defects, one program each in C++, Go,
+This chapter is a menu of five seeded defects, one program each in C++, Go,
and Rust:
```
@@ -8,11 +8,11 @@ app
```
Each subcommand runs a small routine that exhibits exactly one memory or
-concurrency bug — *where the language lets it*. This is not a program with
+concurrency bug, where the language lets it. This is not a program with
a normal happy path: the point of the chapter is that a plain run of most
of these subcommands looks fine (or crashes somewhere unhelpful, far from
the real bug) and it takes the right tool, pointed at the right binary, to
-actually name the defect.
+name the defect.
## What's expressible in each language
@@ -24,23 +24,23 @@ bugs before the program can exist at all.
| bug | C++ | Go | Rust |
|---|---|---|---|
| `leak` | heap alloc, never freed | goroutine blocked forever (leaked) | `Box::leak` (safe API, opts out of reclamation) |
-| `uaf` | use-after-free | — (GC won't free a live reference) | — (borrow checker) |
-| `uninit` | read of indeterminate value | — (every value is zeroed) | — (no way to name an uninitialised value in safe code) |
-| `overflow` | heap buffer overflow, one past the end | — (slice/array access panics, checked) | — (bounds-checked, or rejected at compile time for a fixed index) |
-| `race` | data race on a shared int | data race on a shared int (allowed; GC keeps it memory-safe) | — (`Send`/`Sync` rules reject the unsynchronized sharing) |
+| `uaf` | use-after-free | n/a (GC won't free a live reference) | n/a (borrow checker) |
+| `uninit` | read of indeterminate value | n/a (every value is zeroed) | n/a (no way to name an uninitialised value in safe code) |
+| `overflow` | heap buffer overflow, one past the end | n/a (slice/array access panics, checked) | n/a (bounds-checked, or rejected at compile time for a fixed index) |
+| `race` | data race on a shared int | data race on a shared int (allowed; GC keeps it memory-safe) | n/a (`Send`/`Sync` rules reject the unsynchronized sharing) |
Where a bug isn't expressible, the subcommand still exists (so the CLI
-shape is identical across all three) — it just prints a fixed note and
+shape is identical across all three); it just prints a fixed note and
exits **64** (`EX_USAGE`) instead of attempting anything:
- Go: `go: prevented by the runtime — see chapter`
- Rust: `rust: prevented at compile time — see chapter`
-That message *is* the finding for those cells, not a cop-out: it's the
-same "no bug to reproduce" result you'd get chasing the equivalent C++ bug
-in that language, just arrived at differently (a runtime check vs. a
-rejected compilation) — and it's reproducible non-interactively, which is
-this chapter's whole bar.
+That message is the finding for those cells: the same "no bug to
+reproduce" result you'd get chasing the equivalent C++ bug in that
+language, just arrived at differently (a runtime check vs. a rejected
+compilation), and it's reproducible non-interactively, which is this
+chapter's whole bar.
Bad usage (no subcommand, or an unrecognized one) prints
`usage: app ` on stderr and exits 2, same
@@ -49,8 +49,7 @@ shape as every other chapter's `app`.
## Catching each C++ bug: the tool table
C++ is the only language that expresses all five, so it's the one where
-"run it under the matching tool" actually means something for every
-subcommand:
+"run it under the matching tool" means something for every subcommand:
| bug | tool | build | signature this chapter asserts | exit |
|---|---|---|---|---|
@@ -62,7 +61,7 @@ subcommand:
The `asan` preset was already in this book's CMake template (`-O1 -g
-fno-omit-frame-pointer -fsanitize=address,undefined`); this chapter adds
-a matching `tsan` preset. Neither is part of the `demo.sh` contract —
+a matching `tsan` preset. Neither is part of the `demo.sh` contract:
build them directly:
```console
@@ -75,28 +74,27 @@ $ ./build/tsan/app race
WARNING: ThreadSanitizer: data race (pid=...)
```
-For Rust, valgrind confirms the one expressible bug (`leak`), and — where
-available — miri adds a second, independent confirmation (see below).
+For Rust, valgrind confirms the one expressible bug (`leak`); where
+available, miri adds a second, independent confirmation (see below).
-## A gotcha worth knowing: the optimizer can eat the bug
+## Optimizer effects: the compiler can eliminate the bug
Building `leak`/`uaf`/`uninit`/`overflow` at `-O2` with no barrier, GCC
proved on this host that the allocation was never otherwise observed and
-deleted the whole thing — `malloc`, the write, and all — before valgrind
+deleted the whole thing (`malloc`, the write, and all) before valgrind
ever saw an allocation to lose. Same story in Rust release builds: a
`Box::leak`'d `Vec` whose only observable use is `.len()` (a
compile-time-known constant) can be fully constant-folded away, allocation
included. Every `do_*` function in `cpp/src/main.cpp` routes its pointer
-through a `touch()` helper (`asm volatile("" : : "r,m"(value) : "memory")`
-— the same trick as benchmark libraries' `DoNotOptimize`), and the Rust
+through a `touch()` helper (`asm volatile("" : : "r,m"(value) : "memory")`,
+the same trick as benchmark libraries' `DoNotOptimize`), and the Rust
`leak()` does the same with `std::hint::black_box`. Pull either barrier out
-and re-run valgrind: the leak silently disappears. This is itself a small
-lesson about why "it didn't crash" is not evidence of "there's no bug" —
-the compiler is allowed to make dead code disappear, and an unobserved bug
-is dead code.
+and re-run valgrind: the leak disappears. This is a small lesson about why
+"it didn't crash" is not evidence of "there's no bug": the compiler is
+allowed to make dead code disappear, and an unobserved bug is dead code.
Separately: a plain (uninstrumented) run of C++ `overflow` can abort with
-a glibc `Fatal glibc error: malloc.c:... assertion failed` — the
+a glibc `Fatal glibc error: malloc.c:... assertion failed`: the
one-past-the-end write corrupts the next heap chunk's bookkeeping, and
`delete[]`'s consistency check catches the corruption far from where it
happened. That crash, when it happens, *is* the natural (if unhelpful)
@@ -106,14 +104,14 @@ consequence of the bug; it's why the ASan build exists.
`rust-toolchain.toml` pins `1.97.1`, and `rustup component add miri
--toolchain 1.97.1` fails on this host (`component 'miri' ... is
-unavailable for download for channel '1.97.1'`) — miri tracks nightly, not
+unavailable for download for channel '1.97.1'`): miri tracks nightly, not
stable. Where a `nightly` toolchain with the `miri` component happens to be
installed, `verify.lua` runs it as a **bonus, informational-only** check
(`cargo +nightly miri run -- leak`, expecting `memory leaked` in its
output) and prints what it found; where it isn't installed, it prints why
-it's skipping and moves on. Neither path can fail the example — the
-valgrind check above is what's actually asserted for Rust's `leak`. This
-is exactly the "degrade gracefully" case: miri is a nice extra sample of
+it's skipping and moves on. Neither path can fail the example: the
+valgrind check above is what's asserted for Rust's `leak`. This
+is exactly the "degrade gracefully" case: miri is a useful extra sample of
"a completely different technique (symbolic interpretation instead of
runtime instrumentation) finds the same bug," not a required tool for this
chapter.
@@ -149,8 +147,8 @@ strings and exit codes are what's stable and what `verify.lua` checks.)
Each language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh build` — build only (the plain, uninstrumented binary)
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh build`: build only (the plain, uninstrumented binary)
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
@@ -158,18 +156,17 @@ The top-level `demo.sh` dispatches: `./demo.sh cpp run leak` execs
`cpp/demo.sh run leak`. The binary is always named `app`. Because this
chapter's whole point is tool-assisted detection, the *instrumented*
builds (C++ ASan/TSan presets, the Go `-race` binary) live outside that
-contract — see the tables above for how to build and run them directly.
+contract: see the tables above for how to build and run them directly.
There's no sensible default action for a bare `./demo.sh` (no subcommand)
-here — unlike most chapters, this one has no "just run it" happy path — so
-running with no subcommand prints the usage line and exits 2. That's
-intentional, not an oversight.
+here: unlike most chapters, this one has no "just run it" happy path, so
+running with no subcommand prints the usage line and exits 2.
## Verification
`verify.lua` (run per language with `LSP_LANG` set) builds the plain
-binary, checks the shared bad-usage/unknown-bug behavior, then — per
-language — builds whatever instrumented variant that language's real bugs
+binary, checks the shared bad-usage/unknown-bug behavior, then, per
+language, builds whatever instrumented variant that language's real bugs
need and asserts the detecting tool exits nonzero (66 for both race
detectors' default halt code, 1 otherwise) with the expected signature
string in its output, and that every "prevented" subcommand exits 64 with
diff --git a/examples/30-ebpf-observation-toolkit/README.md b/examples/30-ebpf-observation-toolkit/README.md
index 8236052..7bd1881 100644
--- a/examples/30-ebpf-observation-toolkit/README.md
+++ b/examples/30-ebpf-observation-toolkit/README.md
@@ -1,11 +1,11 @@
-# 30 — eBPF observation toolkit
+# 30: eBPF observation toolkit
-**VM example.** This chapter is not about writing eBPF — it's about pointing
+**VM example.** This chapter is not about writing eBPF: it's about pointing
the standard eBPF *observation* tools (bcc-tools, bpftrace) at an ordinary
userspace program and reading what they say. `app work --seconds N` runs a
small, labeled workload; a driver script (`observe.sh`, running as root on the
lab VM) starts it in the background and then runs five separate tools against
-it in turn, each proving it really saw an event our program produced:
+it in turn, each proving it saw an event our program produced:
| Tool | What it watches | Bait in `app work` |
|---|---|---|
@@ -15,10 +15,10 @@ it in turn, each proving it really saw an event our program produced:
| `offcputime` | off-CPU time by stack | a ~230ms sleep between iterations |
| `bpftrace` | a one-line uprobe program | the same `busy_hash()`, counted with `@calls` |
-`busy_hash()` is deliberately kept unmangled and unstripped, and marked
-noinline, in all three builds — cpp and rust emit the plain symbol
-`busy_hash`, Go emits `main.busy_hash` — so `observe.sh` can target every
-language with the *same* pattern: `:*busy_hash*`.
+`busy_hash()` is kept unmangled and unstripped, and marked noinline, in all
+three builds (cpp and rust emit the plain symbol `busy_hash`, Go emits
+`main.busy_hash`), so `observe.sh` can target every language with the
+*same* pattern: `:*busy_hash*`.
All three languages produce byte-for-byte identical output, including the
final accumulated hash value (same seed, same algorithm, same iteration
@@ -36,20 +36,20 @@ work: done seconds=60 iters=260 opens=260 execs=65 busy_calls=260 busy_hash=1286
Each iteration (~230ms): open/write/close the bait file; every 4th iteration,
fork+exec `true` and wait for it; call `busy_hash()` once; sleep ~230ms. None
-of this needs root — only the *observation* side does.
+of this needs root; only the *observation* side does.
| Language | How the loop runs |
|---|---|
| **C++23** | The loop body runs inside a `std::jthread` (takes a `std::stop_token`, ready for cooperative cancellation); `main()` joins it explicitly so the summary line always sees final counters. An RAII `Fd` class owns the bait file's descriptor. Every fallible syscall returns `std::expected`. Output goes through `std::println`. |
-| **Go 1.26** | The loop runs in a goroutine; `main` blocks on a buffered result channel — the goroutine's "join". Every fallible call is wrapped with `fmt.Errorf("...: %w", err)`. |
-| **Rust (edition 2024)** | Straight-line `Result`/`?` through the whole loop; the bait file's descriptor is a `rustix::fs` `OwnedFd`, closed by `Drop` — no explicit `close(2)`. |
+| **Go 1.26** | The loop runs in a goroutine; `main` blocks on a buffered result channel (the goroutine's "join"). Every fallible call is wrapped with `fmt.Errorf("...: %w", err)`. |
+| **Rust (edition 2024)** | Straight-line `Result`/`?` through the whole loop; the bait file's descriptor is a `rustix::fs` `OwnedFd`, closed by `Drop`, no explicit `close(2)`. |
-### A real optimizer gotcha found while building this
+### An optimizer effect found while building this example
The first version of this workload printed `busy_calls` but never the
accumulated hash value itself. At `-O2`, GCC's interprocedural analysis proved
`busy_hash()` had no observable side effects and that its final result was
-never used anywhere — and deleted the *entire* call chain, `noinline` or not
+never used anywhere, and deleted the *entire* call chain, `noinline` or not
(`noinline` only forbids inlining; it says nothing about dead-code
elimination of a provably-unused, provably-pure call). `funccount` and the
`bpftrace` uprobe both reported zero calls even though the binary clearly
@@ -57,9 +57,9 @@ still exported the `busy_hash` symbol and a plain `nm`/`objdump` looked
completely normal. The fix: print the final hash in the `done` summary line,
in all three languages. The moment the result is observable, the whole chain
is provably necessary again, and the uprobe fires. `busy_hash` is part of the
-printed summary for that reason — it isn't an incidental extra field.
+printed summary to keep the call observable to the uprobe.
-## `observe.sh` — the actual demo
+## `observe.sh`: the demo
`observe.sh [seconds]` runs **on the guest, as root**
(bcc-tools and bpftrace need `CAP_BPF`/`CAP_PERFMON`, which on this lab image
@@ -72,18 +72,18 @@ means root). It:
3. prints a `=== summary ===` block deriving a hit count per tool,
4. lets the workload finish (or stops it) before exiting.
-It deliberately does **not** use `set -e` — a bcc tool killed by its own
-`timeout` wrapper (several of these tools don't reliably self-terminate on
-their internal `-d`/duration flag; see below) is expected, not a driver
-failure. Each step's output is checked on its own, at the end.
+It does **not** use `set -e`: a bcc tool killed by its own `timeout` wrapper
+(several of these tools don't reliably self-terminate on their internal
+`-d`/duration flag; see below) is expected, not a driver failure. Each
+step's output is checked on its own, at the end.
-### A real bcc-tools quirk this driver works around
+### bcc-tools' duration-flag behavior, handled here
`opensnoop`'s own `-d SECONDS` flag did not reliably make it exit on its own
-in testing — it consistently ran until an external `timeout` killed it,
+in testing: it consistently ran until an external `timeout` killed it,
regardless of the requested duration. Every invocation in `observe.sh` is
therefore wrapped in an outer `timeout`, and a `124` exit from that wrapper is
-treated as expected, not a failure — only the captured *content* is asserted
+treated as expected, not a failure; only the captured *content* is asserted
on, never the tool's own exit code.
## CLI
@@ -99,14 +99,14 @@ usage line to stderr and exits `2`.
Each language directory has the standard `demo.sh`:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (`app`) locally
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (`app`) locally
- With env `TARGET` set, `run` deploys to that lab VM via
`scripts/lab/deploy-to-vm.sh` and runs there instead (no root needed for
the workload itself)
The top-level `./demo.sh [cpp|go|rust|all|build]` dispatches per language.
-`observe.sh` is deliberately separate from this dispatcher — it's a custom
+`observe.sh` is kept separate from this dispatcher: it's a custom
root-run driver script (starts the workload *and* orchestrates five different
tools against it), not a single binary+args invocation, so it doesn't fit the
`deploy-to-vm.sh` shape and is staged/run directly instead.
@@ -164,13 +164,13 @@ app_summary: work: done seconds=60 iters=260 opens=260 execs=65 busy_calls=260 b
`verify.lua` (run per language under the runner's **vm mode**,
`TARGET=systems-target`) stages the built binary under `/tmp/lsp30-/app`
-— so its `/proc` comm is exactly `app` regardless of which language built
-it — plus `observe.sh`, then runs the driver as root and asserts, from its
-captured output, that each tool really observed our program:
+(so its `/proc` comm is exactly `app` regardless of which language built
+it) plus `observe.sh`, then runs the driver as root and asserts, from its
+captured output, that each tool observed our program:
1. **opensnoop** shows comm `app` opening the bait file's exact path.
2. **execsnoop** shows a `true` child whose **PPID equals the workload's own
- pid** (parsed from `observe.sh`'s own `workload pid=` line) — not just
+ pid** (parsed from `observe.sh`'s own `workload pid=` line), not just
that *some* exec happened somewhere on the box.
3. **funccount** reports a **nonzero** call count for `busy_hash`.
4. **offcputime** shows a stack naming comm `app` (the ~230ms sleep between
@@ -178,7 +178,7 @@ captured output, that each tool really observed our program:
5. **bpftrace**'s uprobe one-liner reports `@calls` **nonzero**.
Each is a positive count/match on a *specific* signal our program produced,
-not merely "the tool ran and exited 0" — a tool that loads and exits cleanly
+not merely "the tool ran and exited 0." A tool that loads and exits cleanly
while counting nothing would fail every one of these.
Run it:
@@ -188,10 +188,10 @@ LIBVIRT_DEFAULT_URI=qemu:///system \
python3 scripts/test-all-examples.py --only 30-ebpf-observation-toolkit --mode vm
```
-**Why this is real observation, not an inference:** every assertion is tied
-to something our own program did — the exact bait path, the workload's own
-pid as the exec's parent, a call count on the exact function we wrote. This
-is the standard eBPF-based observability toolkit doing what it's designed to
-do: watch an ordinary, unmodified userspace binary from the outside. We write
-no kernel-side eBPF program here — bcc-tools and bpftrace already ship
-theirs; this chapter is about aiming them correctly and reading the answer.
+Every assertion is tied to something our own program did: the exact bait
+path, the workload's own pid as the exec's parent, a call count on the
+exact function we wrote. This is the standard eBPF-based observability
+toolkit doing what it's designed to do: watch an ordinary, unmodified
+userspace binary from the outside. We write no kernel-side eBPF program
+here; bcc-tools and bpftrace already ship theirs. Aiming them correctly and
+reading the answer is the whole exercise.
diff --git a/examples/31-per-language-toolbelts/README.md b/examples/31-per-language-toolbelts/README.md
index b22f3aa..d5a70b2 100644
--- a/examples/31-per-language-toolbelts/README.md
+++ b/examples/31-per-language-toolbelts/README.md
@@ -1,22 +1,22 @@
-# 31-per-language-toolbelts — toolbelt
+# 31-per-language-toolbelts: toolbelt
-Chapter 31 closes the debugging arc by putting each language's *native*
+This chapter closes the debugging arc by putting each language's *native*
profiling toolbelt to work on the same program. There is nothing wrong with
-this code — the point is not to find a defect, but to point a real profiler
-at a real binary and watch it name the function that is actually costing the
+this code; the point is not to find a defect, but to point a real profiler
+at a real binary and watch it name the function that is costing the
cycles or the bytes.
```
app [--n N]
```
-- `hot` — counts primes in `[2, n)` by unoptimized trial division (every
+- `hot`: counts primes in `[2, n)` by unoptimized trial division (every
candidate divisor up to `sqrt(i)`). One function, `spin_hot` /
- `hotSpin`, does all the work — it is the whole point of a CPU profile.
-- `alloc` — builds a 1000-key string index from `n` iterations of
+ `hotSpin`, does all the work; it is the whole point of a CPU profile.
+- `alloc`: builds a 1000-key string index from `n` iterations of
round-robin overwrite, each iteration allocating a fresh key and value
string. One function, `alloc_churn` / `allocChurn`, is the only
- allocation site — it is the whole point of an allocation profile.
+ allocation site; it is the whole point of an allocation profile.
- `--n N` overrides the default size (`hot`: 3,000,000; `alloc`: 200,000);
must be a positive integer.
@@ -32,13 +32,13 @@ app [--n N]
- Bad usage (missing/unknown mode, non-positive `--n`, unknown flag) prints
a diagnostic plus `usage: app [--n N]` on stderr and exits 2.
- Go's binary additionally accepts `--cpuprofile FILE`, `--memprofile FILE`,
- and `--trace FILE` — see "Why Go's CLI has extra flags" below. Its usage
+ and `--trace FILE`; see "Why Go's CLI has extra flags" below. Its usage
banner lists them; C++ and Rust do not accept them (unknown-argument
errors bad-usage the same as any other unrecognized flag).
### Why Go's CLI has extra flags
-`perf` attaches to *any* ELF binary from the outside — nothing in the C++ or
+`perf` attaches to *any* ELF binary from the outside: nothing in the C++ or
Rust source needs to know it is being profiled. Go's CPU and heap profilers
are different: `runtime/pprof` and `runtime/trace` are libraries the program
calls into, so writing a profile is part of the binary's own behavior. That
@@ -61,12 +61,12 @@ app: unknown mode: bogus
usage: app [--n N]
```
-(Timings vary by host and load; `result` does not — same algorithm, same
+(Timings vary by host and load; `result` does not: same algorithm, same
input, same answer, in all three languages.)
-## The toolbelts, run for real
+## The toolbelts in practice
-Every command below was actually run against the binaries in this directory
+Every command below ran against the binaries in this directory
(`cpp/build/release/app`, `go/bin/app`, `rust/target/release/app`) on this
host (Fedora 44, GCC 16.1.1, clang 22.1.8, go1.26.5, rustc 1.97.1, perf
7.1.3, gdb 17.2). Numbers are host- and load-dependent; the shapes and the
@@ -74,7 +74,7 @@ named functions are the point.
### C++: perf record/report, clang-tidy, -fanalyzer
-`spin_hot` has C linkage on purpose (`extern "C"`), so perf and gdb print it
+`spin_hot` has C linkage by design (`extern "C"`), so perf and gdb print it
as the plain symbol `spin_hot` instead of an Itanium-mangled name.
```console
@@ -108,8 +108,8 @@ $ perf report -i cpp.perf.data --stdio
`flamegraph.pl` scripts are not installed on this host (no `FlameGraph/` tree
on `PATH` or under any locatable directory), so per the fallback this chapter
documents, the C++ step stops at `perf report --stdio` above rather than
-producing an SVG. (Compare the Rust section, where `cargo flamegraph` — a
-Rust-native reimplementation of the same collapse+render pipeline — *is*
+producing an SVG. (Compare the Rust section, where `cargo flamegraph`, a
+Rust-native reimplementation of the same collapse+render pipeline, *is*
installable and produces a real SVG.)
`clang-tidy` (bugprone-*, modernize-*, performance-*, readability-*):
@@ -124,14 +124,14 @@ main.cpp:143:5: warning: an exception may be thrown in function 'main' which
One warning, and it is a known shape with `std::format`/`std::println`:
`bugprone-exception-escape` traces a theoretical `std::format_error` path
-that only fires on a malformed format string — ours are literals, so the
+that only fires on a malformed format string; ours are literals, so the
path is unreachable in practice, but the checker cannot prove that
statically. Nothing else fires.
GCC's `-fanalyzer` (its own preset: `cmake --build --preset analyze`) is
-clean — zero diagnostics on this source.
+clean: zero diagnostics on this source.
-### Go: pprof (CPU + heap), go tool trace, delve
+### Go: pprof (CPU + heap), go tool trace, `dlv`
```console
$ ./go/bin/app hot --cpuprofile cpu.prof
@@ -157,12 +157,12 @@ $ go tool pprof --top --cum --alloc_space --nodecount=6 ./go/bin/app mem.prof
`pprof.WriteHeapProfile` runs, the churned map from the just-finished
`allocChurn` call is already garbage (it went out of scope when the
function returned), so the default *in-use* view would show almost nothing.
-`alloc_space` is pprof's cumulative-since-start counter — it remembers
+`alloc_space` is pprof's cumulative-since-start counter: it remembers
everything `mallocgc` ever handed out, freed or not, which is the view that
-actually answers "where did the allocations happen".
+answers "where did the allocations happen".
`go tool trace` opens an interactive, browser-based UI by default, which
-does not script well — but `-d=parsed` dumps the same event stream as text,
+does not script well; `-d=parsed` dumps the same event stream as text,
which is enough to confirm the tracer captured `hotSpin`'s execution:
```console
@@ -173,7 +173,8 @@ $ go tool trace -d=parsed trace.out | grep -B2 hotSpin | head -3
main.hotSpin @ 0x4d272c
```
-Delve, scripted non-interactively (no TUI) with a piped command script:
+The Go debugger, `dlv`, scripted non-interactively (no TUI) with a piped
+command script:
```console
$ echo -e "break main.hotSpin\ncontinue\nprint n\nbt\ncontinue\nquit" \
@@ -187,11 +188,11 @@ $ echo -e "break main.hotSpin\ncontinue\nprint n\nbt\ncontinue\nquit" \
at ./go/main.go:175
```
-(`dlv` is not preinstalled on this host; `go install
-github.com/go-delve/delve/cmd/dlv@latest` pulls it from the Go module proxy
-into `$(go env GOPATH)/bin`. Where that install is unavailable — no module
-proxy reachable — the pprof and trace steps above still stand on their own;
-delve is demonstrated here, not gated on by `verify.lua`.)
+(`dlv` is not preinstalled on this host; `go install github.com/go-delve/delve/cmd/dlv@latest`
+pulls it from the Go module proxy
+into `$(go env GOPATH)/bin`. Where that install is unavailable (no module
+proxy reachable), the pprof and trace steps above still stand on their own;
+`dlv` is demonstrated here, not gated on by `verify.lua`.)
### Rust: perf record/report, cargo flamegraph, clippy
@@ -206,7 +207,7 @@ $ perf report -i rust.perf.data --stdio
spin_hot (inlined)
```
-`cargo-flamegraph` is not preinstalled, but *is* installable —
+`cargo-flamegraph` is not preinstalled, but *is* installable:
`cargo install flamegraph --locked` pulls it (and the `inferno` crate it
wraps, a Rust reimplementation of the classic Perl collapse+render scripts)
from crates.io in well under a minute:
@@ -222,14 +223,14 @@ $ grep -c spin_hot rust-hot.svg
The resulting SVG contains a `spin_hot` frame, confirming the same story
`perf report` tells above, rendered instead of tabulated. Where network
access to crates.io is unavailable, the `perf record`/`perf report --stdio`
-pair above is the fallback path — same data, no plot.
+pair above is the fallback path: same data, no plot.
-`tokio-console`: not applicable here — this program has no async runtime
+`tokio-console` does not apply here: this program has no async runtime
(no `tokio`, no `.await`), so there are no tasks for `tokio-console` to
-show. It belongs to the async chapter (27), not this one.
+show. It belongs to the async runtimes and coroutines chapter, not this one.
`cargo clippy --release --all-targets` and `cargo fmt --check` are both
-clean — no findings.
+clean: no findings.
## Layout
@@ -243,15 +244,15 @@ clean — no findings.
```
The C++ `analyze` preset (`cmake --build --preset analyze`) builds with
-`-fanalyzer` instead of running it as a separate step — it is just another
+`-fanalyzer` instead of running it as a separate step: it is just another
`CMakeLists.txt`-driven build, same as `asan`.
## Demo contract
Each language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (builds first if needed)
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (builds first if needed)
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
@@ -275,7 +276,7 @@ against the real built binary:
value the same algorithm produces in every language (`216816` / `5000`
at the defaults; `4` / `7` at the small sizes).
- `perf stat -e cycles,instructions` on `app hot` exits 0 and its output
- parses two numeric counters — this step is identical for all three
+ parses two numeric counters: this step is identical for all three
languages, since perf attaches to any ELF binary.
- **C++ and Rust**: `perf record` then `perf report --stdio` exits 0 and
the report names `spin_hot`.
@@ -285,7 +286,7 @@ against the real built binary:
`go tool trace`, `delve`, `cargo flamegraph`, `clang-tidy`, and
`-fanalyzer` are demonstrated above with real captured output but are not
-asserted by `verify.lua` — they either open an interactive UI
+asserted by `verify.lua`: they either open an interactive UI
(`go tool trace`'s default mode), depend on a piped TTY session
(`delve`), or depend on a tool this session had to install over the
network (`dlv`, `cargo-flamegraph`) that may not be present in every
@@ -299,15 +300,15 @@ flaky one; the chapter text, not `verify.lua`, carries that evidence.
| Build | `cmake --preset release` + ninja | `go build` | `cargo build --release` |
| CPU profile | `perf record` / `perf report --stdio` | `runtime/pprof` + `go tool pprof --top` | `perf record` / `perf report --stdio`, or `cargo flamegraph` |
| Allocation profile | *(not asserted; `alloc` mode exits 0)* | `runtime/pprof` heap + `go tool pprof --top --cum --alloc_space` | *(not asserted; `alloc` mode exits 0)* |
-| Execution trace | — | `go tool trace -d=parsed` | — |
-| Interactive debugger | `gdb` (chapter 28) | `dlv` | `gdb`/`rust-gdb` (chapter 28) |
+| Execution trace | n/a | `go tool trace -d=parsed` | n/a |
+| Interactive debugger | `gdb` (the gdb chapter) | `dlv` | `gdb`/`rust-gdb` (the gdb chapter) |
| Static analysis | `clang-tidy`, `-fanalyzer` | `go vet` | `cargo clippy` |
| Result asserted by `verify.lua` | `spin_hot` named in `perf report --stdio` | `main.hotSpin`/`main.allocChurn` named in `go tool pprof --top` | `spin_hot` named in `perf report --stdio` |
## Mapping to a chapter
-This example backs chapter 31 (per-language toolbelts), the closing chapter
-of the debugging arc that opened with chapter 28 (gdb and remote debugging)
-and chapter 29 (valgrind, sanitizers, miri). Where those chapters aim a
+This example backs the per-language toolbelts chapter, the closing chapter
+of the debugging arc that opened with the gdb and remote debugging chapter
+and the valgrind/sanitizers/miri chapter. Where those chapters aim a
generic tool at a specific defect, this one aims each language's own
profiler at a workload built to have exactly one obvious answer.
diff --git a/examples/32-namespaces-and-cgroups/README.md b/examples/32-namespaces-and-cgroups/README.md
index 57ea525..0bf9002 100644
--- a/examples/32-namespaces-and-cgroups/README.md
+++ b/examples/32-namespaces-and-cgroups/README.md
@@ -1,4 +1,4 @@
-# 32 — Namespaces & cgroups (`pmon` v6)
+# 32: Namespaces & cgroups (`pmon` v6)
**VM example.** This chapter builds the two primitives every container
runtime is made of, by hand, with no runtime in between: **namespaces** to
@@ -14,8 +14,8 @@ pmon containerize [--hostname NAME] [--mem-max BYTES|max]
It `unshare`s `CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWNET|CLONE_NEWPID`, writes
`memory.max`/`cpu.max` (and disables swap) on a dedicated cgroup *before* CMD
-ever runs, forks the pid-1 child into that setup, and — as the child, before
-handing off to CMD — reports the two things that are otherwise invisible from
+ever runs, forks the pid-1 child into that setup, and (as the child, before
+handing off to CMD) reports the two things that are otherwise invisible from
the outside:
```
@@ -33,15 +33,15 @@ pmon: child exited status= # or: pmon: child killed signal= (` — `flags=0`, `stack=0` gives fork(2) semantics, `exit_signal=SIGCHLD` makes it `waitpid`-able. The child prints its proof lines, flushes, and `execvp`s CMD. `std::expected` carries every syscall failure; RAII (`Fd`) owns every descriptor; single-threaded throughout, on purpose — see the Go entry for what goes wrong when a runtime isn't. |
+| **C++23** | Raw `unshare(2)` (mount/uts/net namespaces land on the caller immediately; the pid namespace is deferred to the next child), then raw `clone3(2)` via ``: `flags=0`, `stack=0` gives fork(2) semantics, `exit_signal=SIGCHLD` makes it `waitpid`-able. The child prints its proof lines, flushes, and `execvp`s CMD. `std::expected` carries every syscall failure; RAII (`Fd`) owns every descriptor; single-threaded throughout, by design; see the Go entry for what goes wrong when a runtime isn't. |
| **Rust (edition 2024)** | `nix::sched::unshare` + `nix::unistd::fork` (the same "no code runs between clone and exec except what we write" shape as C++'s `clone3(flags=0)`), `nix::mount::mount`, `nix::unistd::{sethostname, execvp}`. Errors flow through `Result`/`?`; cgroup file I/O is plain `std::fs`. |
-| **Go 1.26** | The idiomatic inversion, one step further than chapter 14's: Go can't safely `fork()` and keep running Go code in the child (the runtime assumes a live multi-threaded process on both sides), so `unshare(2)` happens on a `runtime.LockOSThread()`-pinned goroutine, and the **fork+exec is a re-exec of pmon itself** (`/proc/self/exe __ns_init__ CMD...`) via `os/exec`, whose fork always runs on the calling goroutine's *current* OS thread — the one we just unshared and pinned. The freshly exec'd pmon is born inside the new namespaces (PID 1 included), prints the proof lines, and `syscall.Exec`s into the real CMD. `LockOSThread` is load-bearing: without it, the goroutine that called `Unshare` could be rescheduled onto a different, un-unshared OS thread before the fork, silently discarding every namespace change. |
+| **Go 1.26** | The idiomatic inversion, one step further than the identity-and-privilege chapter's: Go can't safely `fork()` and keep running Go code in the child (the runtime assumes a live multi-threaded process on both sides), so `unshare(2)` happens on a `runtime.LockOSThread()`-pinned goroutine, and the **fork+exec is a re-exec of pmon itself** (`/proc/self/exe __ns_init__ CMD...`) via `os/exec`, whose fork always runs on the calling goroutine's *current* OS thread: the one we just unshared and pinned. The freshly exec'd pmon is born inside the new namespaces (PID 1 included), prints the proof lines, and `syscall.Exec`s into the real CMD. `LockOSThread` is essential here: without it, the goroutine that called `Unshare` could be rescheduled onto a different, un-unshared OS thread before the fork, silently discarding every namespace change. |
All three: identical CLI, output lines, and exit codes, so one `verify.lua`
covers them.
@@ -77,20 +77,20 @@ covers them.
- **The root cgroup is exempt from the "no internal process" rule.** A
non-root cgroup can't hold processes directly *and* delegate a controller
- to children at the same time — but the root cgroup can, which is exactly
+ to children at the same time, but the root cgroup can, which is exactly
how `containerize` enables `+memory +cpu` in `/sys/fs/cgroup/cgroup.subtree_control`
without first relocating itself into a leaf.
- **Cgroup membership is inherited across fork, not reset by exec.**
`containerize` writes its *own* pid into the new cgroup before unsharing
- or forking, so the pid-1 child — and CMD after it execs — are under the
+ or forking, so the pid-1 child (and CMD after it execs) are under the
limits from their very first instruction.
- **`memory.swap.max=0` makes the limit mean something.** Without it, a
cgroup that hits `memory.max` may just swap and slow down instead of
- triggering the OOM killer — on a host with swap enabled, the negative
+ triggering the OOM killer: on a host with swap enabled, the negative
control would flake. `containerize` sets it (best-effort) alongside
`memory.max`.
- **PSI (`memory.pressure`) is a per-cgroup file**, not a single systemwide
- number — reading `/memory.pressure` after the child exits shows the
+ number: reading `/memory.pressure` after the child exits shows the
stall this specific cgroup experienced, independent of pressure anywhere
else on the box.
@@ -98,10 +98,10 @@ covers them.
Each language directory has the standard `demo.sh`:
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary (`app`)
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary (`app`)
- With env `TARGET` set, `run` deploys to that lab VM via
- `scripts/lab/deploy-to-vm.sh`; set `SUDO=1` — `containerize` needs root for
+ `scripts/lab/deploy-to-vm.sh`; set `SUDO=1`: `containerize` needs root for
`unshare(2)` and cgroup writes.
The top-level `./demo.sh [cpp|go|rust|all|build]` dispatches per language.
@@ -140,12 +140,12 @@ Cross-check from a second guest shell while a `containerize` run is live:
`nsenter -t -p -m -u -n ps -ef` shows the contained process tree with
its own pid 1 and hostname, from the host's side of the fence.
-## A deploy-to-vm.sh gotcha this example works around
+## Keeping CMD tokens simple for deploy-to-vm.sh
`deploy-to-vm.sh` rebuilds the remote command line with a bare `$*`, which
flattens argv and loses any quoting a single argument relied on. A `CMD` like
-`bash -c 'a=$(head -c N /dev/zero | tr ...); sleep 2'` — one argument full of
-spaces and shell metacharacters — arrives on the guest corrupted. Both
+`bash -c 'a=$(head -c N /dev/zero | tr ...); sleep 2'` (one argument full of
+spaces and shell metacharacters) arrives on the guest corrupted. Both
`verify.lua` and the walkthrough above route around it the same way: keep
every `CMD` token free of embedded spaces (`/usr/bin/echo hi`, `sh
/tmp/hog.sh`), staging anything that needs real shell logic as a file on the
@@ -161,14 +161,14 @@ guest first.
`pmon: cgroup mem.pressure some=` line is present, and the run exits 0
with `pmon: child exited status=0`.
2. The guest's **real** hostname is fetched independently over `ssh` and
- asserted **different** from `pmon-vm-check` — the fact pmon is even
+ asserted **different** from `pmon-vm-check`: the fact pmon is even
allowed to compare the two is only interesting because they're both real,
independently-obtained values.
-3. **Negative control** — a real memory hog (`head -c 200000000 /dev/zero |
+3. **Negative control**: a real memory hog (`head -c 200000000 /dev/zero |
tr '\0' x` captured into a shell variable, staged as a guest script) run
under `containerize --mem-max 67108864` (64 MiB, far below the ~190 MiB it
tries to hold) exits **137**, prints `pmon: child killed signal=9 (KILL)`,
- and never reaches its own trailing `echo should-not-print` — proof the
+ and never reaches its own trailing `echo should-not-print`: proof the
cgroup limit terminated it mid-allocation rather than merely slowing it
down.
@@ -180,9 +180,9 @@ LIBVIRT_DEFAULT_URI=qemu:///system \
```
**Why observe the effect, not a trace:** there is no syscall you can
-`strace` from *outside* a process to confirm it believes it's pid 1 — that
+`strace` from *outside* a process to confirm it believes it's pid 1: that
belief is only visible in what the process itself reports, or in what
happens to it. Pairing "reports pid 1" with an independently-fetched host
-hostname, and pairing "cgroup configured" with "hog actually dies", is what
+hostname, and pairing "cgroup configured" with "hog dies", is what
turns three plausible-looking print statements into a demonstration that the
namespaces and the cgroup limit are real.
diff --git a/examples/33-seccomp-and-landlock/README.md b/examples/33-seccomp-and-landlock/README.md
index 545e187..5c745f1 100644
--- a/examples/33-seccomp-and-landlock/README.md
+++ b/examples/33-seccomp-and-landlock/README.md
@@ -1,15 +1,15 @@
-# 33 — seccomp + Landlock (`fwatch` v3)
+# 33: seccomp + Landlock (`fwatch` v3)
`fwatch` gets sandboxed. `watch --sandbox DIR` applies two independent
kernel access-control layers before the epoll/inotify watch loop from
-[chapter 09](../09-event-loops-epoll-inotify) ever runs:
+[the event-loop chapter](../09-event-loops-epoll-inotify) ever runs:
| Layer | What it restricts | Mechanism |
|---|---|---|
-| **Landlock** | filesystem **reads**, to `DIR` only | an unprivileged, self-imposed ruleset the kernel enforces against this process and every descendant — it cannot be lifted short of exec-ing something that never asked for it |
+| **Landlock** | filesystem **reads**, to `DIR` only | an unprivileged, self-imposed ruleset the kernel enforces against this process and every descendant; it cannot be lifted short of exec-ing something that never asked for it |
| **seccomp** | which **syscalls** may run at all | a syscall allowlist covering only what the watch loop needs; anything else returns `EPERM` |
-Two `probe` subcommands are negative controls that apply ONE layer in
+Two `probe` subcommands are negative controls that apply *one* layer in
isolation and prove it denies exactly what it claims to:
```
@@ -32,13 +32,13 @@ the sandbox. The Go implementation avoids this with `runtime.LockOSThread()`
(pins the sandbox-installing goroutine to a stable OS thread) plus the
`LANDLOCK_RESTRICT_SELF_TSYNC` and `SECCOMP_FILTER_FLAG_TSYNC` flags, which
apply the new state to **every** thread already running in the process, not
-just the caller's — confirmed locally: a goroutine deliberately scheduled
-after the sandbox is installed still gets EACCES/EPERM.
+just the caller's. Confirmed locally: a goroutine scheduled after the
+sandbox is installed still gets EACCES/EPERM.
The Landlock ABI has no libc/x/sys wrapper in any of the three languages;
every version probes it at runtime with
`landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)` rather
-than assuming a version — the printed `ABI=` is a live kernel query, not a
+than assuming a version: the printed `ABI=` is a live kernel query, not a
constant.
## CLI
@@ -53,15 +53,16 @@ usage: fwatch
probe --forbidden-syscall negative control: socket(2) under a seccomp allowlist that omits it
```
-- `snapshot` / `diff` / unsandboxed `watch` are unchanged from chapter 09.
-- `watch --sandbox DIR` — applies Landlock (read+watch access restricted to
+- `snapshot` / `diff` / unsandboxed `watch` are unchanged from the event-loop
+ chapter.
+- `watch --sandbox DIR`: applies Landlock (read+watch access restricted to
`DIR`) then the seccomp allowlist, prints both status lines to stderr, then
runs the identical watch loop. Events, debounce, and the
- `(timeout)`/`(signal)` exit lines are exactly as in chapter 09 — the
- sandbox is invisible to the loop's own logic.
-- `probe --sandbox DIR --outside PATH` — restricts Landlock to `DIR`, then
+ `(timeout)`/`(signal)` exit lines are exactly as in the event-loop chapter;
+ the sandbox is invisible to the loop's own logic.
+- `probe --sandbox DIR --outside PATH`: restricts Landlock to `DIR`, then
attempts to open `PATH` (expected to be outside `DIR`) for reading.
-- `probe --forbidden-syscall` — installs the same seccomp allowlist (which
+- `probe --forbidden-syscall`: installs the same seccomp allowlist (which
never includes `socket(2)`), then calls `socket(AF_INET, SOCK_STREAM, 0)`.
### Exit codes
@@ -71,8 +72,8 @@ usage: fwatch
| `0` | success (snapshot/diff/watch, including a sandboxed watch's clean timeout/signal exit) |
| `1` | runtime error, or a probe hit something other than the expected denial |
| `2` | usage error |
-| `20` | **probe confirmed denial** — the sandbox blocked what it should (the passing case for a negative control) |
-| `21` | **probe was NOT denied** — the sandbox failed to block what it should (a real sandbox bug) |
+| `20` | **probe confirmed denial**: the sandbox blocked what it should (the passing case for a negative control) |
+| `21` | **probe was *not* denied**: the sandbox failed to block what it should (a real sandbox bug) |
## Try it
@@ -88,7 +89,7 @@ wait # landlock/seccomp status lines, then the same event stream as ch09
./cpp/build/release/app probe --sandbox /tmp/fw-sandboxed --outside /etc/hostname; echo "exit=$?" # EACCES, exit 20
```
-Landlock and seccomp are both **unprivileged** self-sandboxing mechanisms —
+Landlock and seccomp are both **unprivileged** self-sandboxing mechanisms:
no root is needed to run any of the above on a Linux 5.13+ kernel with
Landlock enabled.
@@ -100,10 +101,10 @@ runs the driver as root (the book's convention for every
namespace/cgroup/seccomp/Landlock demo, even though these two mechanisms
don't themselves require it). The driver exercises, in one ssh round trip:
-1. `probe --forbidden-syscall` — asserts `EPERM` and exit `20`.
-2. `probe --sandbox ... --outside ...` — asserts `EACCES` and exit `20`.
+1. `probe --forbidden-syscall`: asserts `EPERM` and exit `20`.
+2. `probe --sandbox ... --outside ...`: asserts `EACCES` and exit `20`.
3. `watch --sandbox ...` with a file created/modified/deleted inside the
- tree while it runs — asserts an `event: ... inside.txt` line, the
+ tree while it runs: asserts an `event: ... inside.txt` line, the
`(timeout)` exit line, and exit `0` (the positive control: sandboxing
doesn't break the watcher).
diff --git a/examples/34-our-programs-in-containers/README.md b/examples/34-our-programs-in-containers/README.md
index 1668207..1d7bafc 100644
--- a/examples/34-our-programs-in-containers/README.md
+++ b/examples/34-our-programs-in-containers/README.md
@@ -1,7 +1,7 @@
-# 34 — our programs in containers
+# 34: our programs in containers
-`app`, a tiny PID-1 container entrypoint, implemented three times — C++23,
-Go, and Rust — each built into its own multi-stage **Containerfile** (UBI10
+`app`, a tiny PID-1 container entrypoint, implemented three times, in C++23,
+Go, and Rust, each built into its own multi-stage **Containerfile** (UBI10
builder → UBI10-micro runtime) and run under **rootless podman**. It
demonstrates three things a program running as a container's entrypoint has
to get right that a normal program never has to think about:
@@ -11,17 +11,17 @@ usage: app serve|naive # C++, Go, Rust
usage: app serve|naive|worker|job # Go, Rust (self-reexec children; see below)
```
-1. **pid-1 signal duties** — `serve` reaps every child (so short-lived jobs
+1. **pid-1 signal duties**: `serve` reaps every child (so short-lived jobs
never pile up as zombies) and forwards `SIGTERM` to the worker it
supervises before exiting. `naive` installs no signal handling at all, the
mistake this example exists to make visible.
-2. **container-aware resource detection** — `serve`/`naive` read their own
+2. **container-aware resource detection**: `serve`/`naive` read their own
cgroup v2 `cpu.max`/`memory.max` and print
`container: cpu.max= effective_parallelism= mem.max=`. Go's
`GOMAXPROCS` and Rust's `available_parallelism()` are cgroup-aware; C++'s
- `hardware_concurrency()` is not — it reports the **host's** cpu count
+ `hardware_concurrency()` is not: it reports the **host's** cpu count
however small `--cpus` is set. That mismatch is the trap.
-3. **debugging across namespaces** — `podman exec` and a rootless
+3. **debugging across namespaces**: `podman exec` and a rootless
`podman unshare nsenter` from the host both reach the same PID 1 inside
the running container.
@@ -64,18 +64,18 @@ resulting binary needs only `libc`/`libgcc_s`, both already in
| stdout | `app: naive heartbeat tick=` | `naive`, every 1s, forever |
| stdout | `app: shutting down (SIGTERM\|SIGINT)` | `serve` only, right before it exits 0 |
-`naive` never prints a shutdown line — that absence is itself the evidence
+`naive` never prints a shutdown line: that absence is itself the evidence
the example verifies.
`cpu.max` is printed as `max` (unconstrained) or `/` (derived
from the cgroup file's raw `" "` content); `mem.max` is
printed exactly as the cgroup reports it (`max` or a byte count).
-## The trap, reproduced for real
+## The trap, confirmed
Run with `podman run --cpus=2 --memory=128m`, all three correctly report the
container's `cpu.max=200000/100000` and `mem.max=134217728` (not the host's
-`max`/`max`) — but `effective_parallelism` tells a different story per
+`max`/`max`), but `effective_parallelism` tells a different story per
language, captured from a real run on a 16-cpu host:
```
@@ -88,43 +88,44 @@ C++'s `std::thread::hardware_concurrency()` is `sysconf(_SC_NPROCESSORS_ONLN)`
under the hood: it has no idea a cgroup exists. Go's `GOMAXPROCS` (default
container-aware as of the `go 1.26` language version) and Rust's
`available_parallelism()` both divide the cgroup's cpu quota by its period
-and report `2` — the number that is actually true.
+and report `2`, the number that matches the actual constraint.
## Per-language mechanics
-- **C++23** (`cpp/src/main.cpp`) — raw `fork(2)` for both the worker and each
+- **C++23** (`cpp/src/main.cpp`): raw `fork(2)` for both the worker and each
job (no exec: they are just different functions in the same binary). This
- is the one place a subtle, very real bug lived during development: the
- forked worker **inherits the parent's blocked signal mask**
+ is the one place where signal-mask inheritance needs explicit handling:
+ the forked worker **inherits the parent's blocked signal mask**
(`sigprocmask(SIG_BLOCK, ...)` for `SIGTERM`/`SIGINT`/`SIGCHLD`, set up so
- `serve` can receive them via `signalfd(2)`). Left alone, a forwarded
+ `serve` can receive them via `signalfd(2)`). Left unmasked, a forwarded
`SIGTERM` to the worker would just sit *pending* forever instead of
- running its default (terminate) action — the worker would be unkillable.
- The fix is one `sigprocmask(SIG_SETMASK, &empty, ...)` in the child right
- after `fork()`, the same principle as pmon's (ch. 12) reset-before-`execvp`,
- just with no `exec` to do it for you. `reap_all()` drains
- `waitpid(-1, WNOHANG)` from the ordinary `poll(2)` loop (never from signal
- context) whenever the `signalfd` reports `SIGCHLD`. A `std::jthread` (with
- a `stop_token`, RAII-joined) drives the periodic job spawns.
-- **Go 1.26** (`go/main.go`) — no `fork(2)`-without-exec (Go's runtime is not
+ running its default (terminate) action, leaving the worker unkillable.
+ One `sigprocmask(SIG_SETMASK, &empty, ...)` in the child right
+ after `fork()` resets it, the same principle as the signal-handling
+ chapter's `pmon` reset-before-`execvp`, just with no `exec` to do it for
+ you. `reap_all()` drains `waitpid(-1, WNOHANG)` from the ordinary
+ `poll(2)` loop (never from signal context) whenever the `signalfd`
+ reports `SIGCHLD`. A `std::jthread` (with a `stop_token`, RAII-joined)
+ drives the periodic job spawns.
+- **Go 1.26** (`go/main.go`): no `fork(2)`-without-exec (Go's runtime is not
fork-safe once other goroutines/threads exist), so the worker and each job
are separate processes started by **re-executing the same binary**
(`os.Executable()` + `exec.Command(self, "worker"|"job", ...)`). No
`SIGCHLD`/`signalfd` anywhere: `Cmd.Wait()` calls `wait4(2)` for you, so one
- goroutine parked in `Wait()` per spawned child *is* the reap loop. A real,
- verified surprise from building this: a Go binary that never calls
- `signal.Notify` still **dies immediately** on `SIGTERM`, even as PID 1 —
+ goroutine parked in `Wait()` per spawned child *is* the reap loop. A
+ notable behavior from building this: a Go binary that never calls
+ `signal.Notify` still **dies immediately** on `SIGTERM`, even as PID 1:
Go's runtime installs its own handler for fatal signals like `SIGTERM` at
startup regardless of user code (see `runtime`'s internal signal table),
so it never has an unclaimed `SIG_DFL` disposition for the kernel's PID-1
special case to mask out. `naive` in Go is therefore not "unresponsive" the
- way C++'s/Rust's are — it is abrupt instead: no reaping, no forwarding, no
+ way C++'s/Rust's are; it is abrupt instead: no reaping, no forwarding, no
shutdown message, just an instant, uncontrolled death.
-- **Rust edition 2024** (`rust/src/main.rs`) — same self-reexec approach as
+- **Rust edition 2024** (`rust/src/main.rs`): same self-reexec approach as
Go, via `std::env::current_exe()` + `std::process::Command`, for the same
reason (no fork-without-exec once threads exist). `signal_hook::iterator::
Signals::new([SIGTERM, SIGINT])` turns both into a blocking iterator on the
- main thread — no polling, no manual mask bookkeeping, and (unlike the
+ main thread: no polling, no manual mask bookkeeping, and (unlike the
`sigprocmask`-based approach) nothing to reset before `exec`, since
`signal_hook` only installs handlers and never blocks the signals it
watches. Reaping is `std::thread::spawn` + `Child::wait()` per job, exactly
@@ -158,39 +159,39 @@ podman rm -f demo34; podman rmi -f lsp34-go
```
`nsenter` directly (without `podman unshare`) fails rootless with
-`Operation not permitted` — the container's namespaces live inside podman's
+`Operation not permitted`: the container's namespaces live inside podman's
own unprivileged user namespace, and `podman unshare` is what puts you inside
it first.
## Verification (behavioral)
`verify.lua` requires podman on the host (`skip()`, exit 77, if it is
-missing) and, for every language, actually builds the image and drives real
+missing) and, for every language, builds the image and drives real
containers rather than trusting exit codes:
-- **Resource detection under real constraints** — `podman run --cpus=2
+- **Resource detection under real constraints**: `podman run --cpus=2
--memory=128m`, then the container's own logs must show the *constrained*
- `cpu.max=200000/100000` and `mem.max=134217728` — never the host's `max`.
+ `cpu.max=200000/100000` and `mem.max=134217728`, never the host's `max`.
C++'s `effective_parallelism` must equal the **host's** live `nproc` (the
- trap, reproduced); Go's and Rust's must equal `2` (the cgroup quota). The
+ trap, confirmed); Go's and Rust's must equal `2` (the cgroup quota). The
host must report `nproc >= 3` or the test skips, since otherwise C++'s
"wrong" number would coincidentally equal the "right" one.
-- **PID-1 identity and reaping** — `app: pid=1 ppid=0` in the logs; several
+- **PID-1 identity and reaping**: `app: pid=1 ppid=0` in the logs; several
`app: reaped pid=... status=0` lines with no pile-up; a `podman exec`
zombie scan of `/proc/[0-9]*/stat` reports `zombies=0` while jobs are being
spawned and reaped.
-- **Cross-namespace debugging** — `podman exec ... cat /proc/1/status` names
+- **Cross-namespace debugging**: `podman exec ... cat /proc/1/status` names
the process `app`; a rootless `podman unshare nsenter --target
--pid --mount -- readlink /proc/1/exe` from the host resolves to the same
`/usr/local/bin/app`.
-- **`serve` vs `naive`, timed** — `podman stop -t 3` on `serve` completes in
+- **`serve` vs `naive`, timed**: `podman stop -t 3` on `serve` completes in
well under a second (real runs: ~90-220ms) with a `shutting down (SIGTERM)`
line in the logs. `podman stop -t 2` on `naive`: for C++/Rust this takes
the **full grace period** (~2.1s) and podman's own log shows it fell back
to `SIGKILL`; for Go it stops fast anyway (~90ms) for the runtime reason
above, but in every language the shutdown line **never appears** in
`naive`'s logs, proving no graceful path ran regardless of how the process
- actually died.
+ died.
- Every container and the built image are removed at the end, whether the
checks passed or not.
diff --git a/examples/35-virtualization-and-kvm/README.md b/examples/35-virtualization-and-kvm/README.md
index aab84bb..7d8f525 100644
--- a/examples/35-virtualization-and-kvm/README.md
+++ b/examples/35-virtualization-and-kvm/README.md
@@ -1,8 +1,8 @@
-# 35-virtualization-and-kvm — overheadbench
+# 35-virtualization-and-kvm: overheadbench
-Chapter 35's example: `overheadbench`, three tiny host-local microbenchmarks
+This chapter's example: `overheadbench`, three tiny host-local microbenchmarks
implemented identically in C++23, Go, and Rust. The example itself only
-measures **wherever it is run** — it makes no VM/container comparison on its
+measures **wherever it is run**: it makes no VM/container comparison on its
own. The chapter's point is made by running the *same binary* in three
places (bare host, a lab KVM guest, and a rootless podman container) and
tabulating the numbers side by side.
@@ -21,20 +21,20 @@ bench=mem metric=14.03 unit=GB/s
bench=io metric=1480.21 unit=ops/s
```
-- **`syscall`** — a tight `getpid(2)` loop (a raw syscall each iteration, not
+- **`syscall`**: a tight `getpid(2)` loop (a raw syscall each iteration, not
a libc-cached value). Reports **ns/call**: mean wall time per syscall.
This is the number the chapter contrasts against a VM's vmexit-trap cost
and a container's near-zero added cost (containers share the host kernel;
a syscall inside one *is* a host syscall).
-- **`mem`** — allocates a 128 MiB buffer of `u64` words and reads it
+- **`mem`**: allocates a 128 MiB buffer of `u64` words and reads it
sequentially, summing every word, for a configurable number of passes.
Reports **GB/s**: total bytes read across all passes divided by elapsed
time. The sum is consumed after the clock stops so the compiler can't
prove the scan is dead code and delete it.
-- **`io`** — repeats a create → write(4096 bytes) → `fsync(2)` → unlink
+- **`io`**: repeats a create → write(4096 bytes) → `fsync(2)` → unlink
cycle. Reports **ops/s**: complete cycles per second. The scratch
directory is created **relative to the current working directory**
- (where `demo.sh` runs the binary from), not the system temp directory —
+ (where `demo.sh` runs the binary from), not the system temp directory:
on many dev hosts `/tmp` is `tmpfs`, where `fsync` is nearly free and the
number stops meaning anything as "storage overhead." Comparing this
number meaningfully across host/VM/container requires each side to write
@@ -50,7 +50,7 @@ milliseconds): `syscall` 200,000 calls, `mem` 16 passes, `io` 200 cycles.
Bad usage (an unknown `--bench` value, a flag with a missing value, an
unknown flag, or a non-numeric/zero `--iters`) prints
`usage: overheadbench [--bench syscall|mem|io|all] [--iters N]` to stderr
-and exits 2 — never a silently wrong number.
+and exits 2, never a silently wrong number.
## Layout
@@ -68,12 +68,12 @@ and exits 2 — never a silently wrong number.
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run (`--bench all`)
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary with the given arguments
+- `./demo.sh`: build then run (`--bench all`)
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary with the given arguments
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
- running locally — this is how the chapter gets the VM-side numbers.
+ running locally. This is how the chapter gets the VM-side numbers.
## Verification
@@ -85,18 +85,18 @@ a time. It asserts, per language:
in order `syscall`, `mem`, `io`, each parsing as `bench=
metric= unit=` with the metric inside a generous but real
sanity window (e.g. `syscall` between 1 ns and 5 ms per call, `mem`
- between 10 MB/s and 1 TB/s, `io` between 0.1 and 5,000,000 ops/s) — wide
+ between 10 MB/s and 1 TB/s, `io` between 0.1 and 5,000,000 ops/s), wide
enough to tolerate a loaded CI host or unusual hardware, narrow enough to
catch a zeroed-out, negative, or nonsensical measurement;
- selecting a single bench with `--bench` prints only that bench's row;
- `--iters` with a small explicit value (e.g. 10 or 500) still produces a
- sane, parseable row — guarding against a divide-by-zero at low iteration
+ sane, parseable row, guarding against a divide-by-zero at low iteration
counts;
- bad usage (unknown `--bench` value, missing flag value, unknown flag,
non-numeric or zero `--iters`) exits 2 and prints the usage line.
Real host numbers from this machine (Fedora 44, kernel 7.1,
-`examples/` on an NVMe-backed btrfs filesystem — not `/tmp`):
+`examples/` on an NVMe-backed btrfs filesystem, not `/tmp`):
| lang | syscall (ns/call) | mem (GB/s) | io (ops/s) |
|------|-------------------:|-----------:|-----------:|
@@ -104,21 +104,22 @@ Real host numbers from this machine (Fedora 44, kernel 7.1,
| go | 90–159 | 12–14 | 1,450–1,670 |
| rust | 87–96 | 16–19 | 1,260–1,580 |
-The exact numbers vary run to run — that's expected of a microbenchmark;
+The exact numbers vary run to run; that's expected of a microbenchmark;
`verify.lua` checks shape and sane range, not a fixed value.
## What the chapter does with it (not asserted by `verify.lua`)
1. Run `./demo.sh run` on the bare host (the numbers above).
2. Deploy the same binary to `systems-target`
- (`TARGET=systems-target ./demo.sh run`) and record the VM numbers —
+ (`TARGET=systems-target ./demo.sh run`) and record the VM numbers:
expect a real `syscall` gap (vmexit/vmenter round trip on every trapped
instruction) and comparable `mem`/`io` numbers once the guest is warmed
up, modulo whatever the host's virtio/virtualization layer costs for
storage.
-3. Build the chapter-34 UBI 10 Containerfile for the same language, run the
- binary with `podman run --rm ` on the host, and record the
- container numbers — expect `syscall` and `mem` close to bare-host (a
+3. Build the containers chapter's UBI 10 Containerfile for the same
+ language, run the binary with `podman run --rm ` on the host, and
+ record the container numbers: expect `syscall` and `mem` close to
+ bare-host (a
container's process runs directly on the host kernel and CPU), with `io`
depending on the container's storage driver/overlay filesystem.
4. Tabulate host vs VM vs container for the pros/cons discussion: containers
diff --git a/examples/36-proc-sys-and-the-agent/README.md b/examples/36-proc-sys-and-the-agent/README.md
index 8e420ab..b96d545 100644
--- a/examples/36-proc-sys-and-the-agent/README.md
+++ b/examples/36-proc-sys-and-the-agent/README.md
@@ -1,8 +1,8 @@
-# 36-proc-sys-and-the-agent — sysagent v0
+# 36-proc-sys-and-the-agent: sysagent v0
Chapter 36 opens Part 9 (observability). `sysagent` is a small USE-method
-metrics collector — the same tool the rest of the Part builds on (chapter 37's
-saturation drills, chapter 41's capstone fleet) — reading nothing but
+metrics collector, the same tool the rest of the Part builds on (later
+saturation drills and the final fleet exercise), reading nothing but
`/proc`, `/proc/net`, `/proc/diskstats`, and cgroup PSI. No root, no
`/dev/kmsg`, no eBPF: everything here is a plain file a normal user can open.
@@ -18,23 +18,23 @@ opts: --interval-ms N (default 200) cpu utilization sampling window
- **`sample`** takes exactly one snapshot: it reads `/proc/stat` once, sleeps
`--interval-ms`, reads it again, and reports the CPU delta over that
- window — alongside `/proc/loadavg`, `/proc/meminfo`, `/proc/diskstats`,
+ window, alongside `/proc/loadavg`, `/proc/meminfo`, `/proc/diskstats`,
`/proc/net/dev`, and cgroup PSI, all gathered **concurrently** with the
sleep (C++ `std::jthread`, Go goroutines fanned in over a channel, Rust
- scoped threads — three languages, one idea: don't serialize five
+ scoped threads, three languages, one idea: don't serialize five
independent file reads behind a timer). Default text output is
`key=value` lines; `--json` emits the single-line JSON object below.
- **`serve --port P`** exposes the identical snapshot over a hand-rolled
HTTP/1.1 `GET /metrics` endpoint (Go uses `net/http`; C++ and Rust hand-roll
the same tiny surface on raw sockets + `poll(2)`, in keeping with the
- book's earlier chatterd examples) — one fresh sample per request. Any
+ book's earlier chatterd examples), serving one fresh sample per request. Any
other path 404s. `SIGINT`/`SIGTERM` prints a shutdown line and exits 0.
- No/bad arguments print a usage line on stderr and exit 2.
## The deterministic field schema
Every field name below is byte-identical across `--json` and `/metrics` in
-all three languages — that's the contract this example exists to prove out:
+all three languages; that's the contract this example exists to prove out:
you can point one Grafana dashboard or one test suite at any of the three.
| field | type | source |
@@ -62,13 +62,13 @@ readings (a single reading of `/proc/stat` is a cumulative counter since
boot, not a percentage).
PSI (Pressure Stall Information) is read from **this process's own cgroup**
-first — resolved via the single `0::` line in `/proc/self/cgroup`
-(cgroup v2 unified hierarchy) joined onto `/sys/fs/cgroup/{cpu,memory,io}.pressure`
-— falling back to the system-wide `/proc/pressure/{cpu,memory,io}` files if
+first, resolved via the single `0::` line in `/proc/self/cgroup`
+(cgroup v2 unified hierarchy) joined onto `/sys/fs/cgroup/{cpu,memory,io}.pressure`,
+falling back to the system-wide `/proc/pressure/{cpu,memory,io}` files if
the cgroup-scoped ones aren't there. If neither is readable (PSI disabled in
the kernel, or a container without a cgroup-v2 delegate), `psi_available` is
-`false` and the three `avg10` fields are `0.0` — that's the "if available"
-in the chapter spec: a missing PSI source is not an error.
+`false` and the three `avg10` fields are `0.0`, matching the "if available"
+behavior in the chapter spec: a missing PSI source is not an error.
## Example output
@@ -91,25 +91,25 @@ $ kill -INT %1
sysagent: shutting down
```
-(That `sample` line reflects real load on the box this was written on — a
+(That `sample` line reflects real load on the box this was written on: a
concurrent `cc1plus`-heavy build was running in the background, which is
exactly the kind of noise the verification below is built to tolerate.)
## One subject, three concurrency styles
-The `/proc` parsing itself is deliberately unglamorous — `open`, read lines,
-split fields — so the interesting difference is how each language overlaps
+The `/proc` parsing itself is unglamorous (`open`, read lines,
+split fields), so the interesting difference is how each language overlaps
the five independent reads with the CPU-delta sampling window instead of
doing five sequential blocking calls:
-- **C++** — `std::jthread` per source (`loadavg`, `meminfo`, `diskstats`,
+- **C++**: `std::jthread` per source (`loadavg`, `meminfo`, `diskstats`,
`netdev`, `psi`), an `std::atomic` success counter, and a small
`ProcFile` RAII wrapper around `std::ifstream` so every `/proc` handle
closes itself. Errors flow through `std::expected`.
-- **Go** — one goroutine per source, fanned in over a single buffered
- `chan namedErr` — the idiomatic Go answer to "wait for N concurrent
+- **Go**: one goroutine per source, fanned in over a single buffered
+ `chan namedErr`, the idiomatic Go answer to "wait for N concurrent
results and keep the first error." Errors are wrapped with `%w` throughout.
-- **Rust** — `std::thread::scope` with one scoped closure per source, so the
+- **Rust**: `std::thread::scope` with one scoped closure per source, so the
borrows into the final `Snapshot` don't need `Arc`/`Mutex`; each closure
returns a `Result` collected via `JoinHandle::join()`. Errors
flow through `Result`/`?`.
@@ -117,7 +117,7 @@ doing five sequential blocking calls:
The HTTP layer follows the same three-languages-one-idea pattern as the
book's chatterd examples: Go leans on its stdlib `net/http`; C++ and Rust
hand-roll the request line + `poll(2)` + `signalfd`/`SignalFd` shutdown
-doorbell that chapter 24 already established, because there's no reason to
+doorbell established earlier in the book's chatterd chapters, because there's no reason to
pull in a framework for one endpoint.
## Layout
@@ -136,7 +136,7 @@ pull in a framework for one endpoint.
Each language directory has a `demo.sh` with the identical interface used
book-wide: `./demo.sh build`, `./demo.sh run [args]`, and a bare `./demo.sh`
that builds then runs. With `TARGET` set, `run` deploys to that lab VM
-instead of running locally — unused here since this chapter is local-only
+instead of running locally; unused here since this chapter is local-only
(`mode: local` in the manifest); it reads the current host's `/proc`, not a
VM's. `run` execs straight into the built binary so signals (`SIGINT` on
`serve`) reach it directly rather than a wrapping shell.
@@ -146,8 +146,8 @@ VM's. `run` execs straight into the built binary so signals (`SIGINT` on
`verify.lua` (run by the test harness with `LSP_LANG` set to one language at
a time) asserts observable behavior, not exit 0:
-1. Usage errors — no arguments, an unknown subcommand, `serve` without
- `--port` — all exit 2 with a usage line.
+1. Usage errors (no arguments, an unknown subcommand, `serve` without
+ `--port`) all exit 2 with a usage line.
2. `sample --json` carries every field in the schema above with a numeric
(or boolean, for `psi_available`) value, `mem_total_kb` is positive,
`cpu_util_pct` is in `[0, 100]`, the `disks` array has a named entry, and
@@ -157,7 +157,7 @@ a time) asserts observable behavior, not exit 0:
longer than a `sample --interval-ms 500` window and compares against an
immediately-prior idle sample, retried up to three times with a 12
percentage-point margin (or an absolute ≥55% floor, for the case where the
- host is already saturated) — robust against running on a real, shared
+ host is already saturated), robust against running on a real, shared
development box rather than an isolated benchmark rig.
4. `serve --port P` answers `GET /metrics` with the same JSON shape, 404s an
unknown path, and its `mem_total_kb` agrees **exactly** with a direct
diff --git a/examples/37-use-method-60s-analysis/README.md b/examples/37-use-method-60s-analysis/README.md
index 39b523c..6c41638 100644
--- a/examples/37-use-method-60s-analysis/README.md
+++ b/examples/37-use-method-60s-analysis/README.md
@@ -1,7 +1,7 @@
-# _template — hello-syscall
+# _template: hello-syscall
The seed for every runnable example in the book. It implements the same tiny
-program three times — C++23, Go, and Rust — each printing exactly one line:
+program three times, in C++23, Go, and Rust, each printing exactly one line:
```
pid on ()
@@ -26,9 +26,9 @@ _template/
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
diff --git a/examples/38-opentelemetry-to-lgtm/README.md b/examples/38-opentelemetry-to-lgtm/README.md
index 39b523c..6c41638 100644
--- a/examples/38-opentelemetry-to-lgtm/README.md
+++ b/examples/38-opentelemetry-to-lgtm/README.md
@@ -1,7 +1,7 @@
-# _template — hello-syscall
+# _template: hello-syscall
The seed for every runnable example in the book. It implements the same tiny
-program three times — C++23, Go, and Rust — each printing exactly one line:
+program three times, in C++23, Go, and Rust, each printing exactly one line:
```
pid on ()
@@ -26,9 +26,9 @@ _template/
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
diff --git a/examples/39-benchmarking-without-lies/README.md b/examples/39-benchmarking-without-lies/README.md
index 1df6226..bc78c2b 100644
--- a/examples/39-benchmarking-without-lies/README.md
+++ b/examples/39-benchmarking-without-lies/README.md
@@ -1,8 +1,9 @@
# 39-benchmarking-without-lies
Chapter 39's example: `benchlab`, a microbenchmark harness for the chatterd
-frame codec (`encode_frame`/`decode_frame`, introduced ch21) — implemented
-three times, C++23, Go, and Rust, with one shared observable contract. The
+frame codec (`encode_frame`/`decode_frame`, introduced with the TCP chatterd
+server), implemented three times, C++23, Go, and Rust, with one shared
+observable contract. The
point of this chapter is not the codec; it's the two different ways to
measure it, side by side in the same binary.
@@ -13,19 +14,19 @@ benchlab --op encode|decode|roundtrip --iters N [--warmup W] # the real thing
benchlab --lie [--op encode|decode|roundtrip] # the anti-pattern
```
-`--op` selects what's timed against a fixed workload — one chatterd `DELIVER`
+`--op` selects what's timed against a fixed workload: one chatterd `DELIVER`
frame (`alice\0the quick brown fox jumps over the lazy dog, three times, for
benchlab`):
-- `encode` — build the frame from the payload bytes.
-- `decode` — parse a pre-built frame back into `(type, payload)`.
-- `roundtrip` — both, per iteration.
+- `encode`: build the frame from the payload bytes.
+- `decode`: parse a pre-built frame back into `(type, payload)`.
+- `roundtrip`: both, per iteration.
### The real thing
-`--iters N --warmup W` (default `W=1000`) runs `W` untimed warmup iterations
-— letting the allocator, branch predictor, and (on Go) the runtime reach
-something like steady state — then times each of the next `N` iterations
+`--iters N --warmup W` (default `W=1000`) runs `W` untimed warmup iterations,
+letting the allocator, branch predictor, and (on Go) the runtime reach
+something like steady state, then times each of the next `N` iterations
individually with a monotonic clock, and reports:
```
@@ -36,26 +37,26 @@ benchlab: co_p99_ns=914420 expected_interval_ns=87 co_n=113003
benchlab: checksum=000000000079c3e0
```
-- `min_ns`/`median_ns`/`p99_ns`/`max_ns` — the raw latency distribution in
+- `min_ns`/`median_ns`/`p99_ns`/`max_ns`: the raw latency distribution in
nanoseconds, nearest-rank percentiles over the sorted sample.
-- `co_p99_ns` — the **coordinated-omission-corrected** p99 (see below);
+- `co_p99_ns`: the **coordinated-omission-corrected** p99 (see below);
always `>= p99_ns`.
-- `expected_interval_ns` — the interval the correction assumes, derived from
+- `expected_interval_ns`: the interval the correction assumes, derived from
the run's own raw median (documented, not a hidden knob).
-- `co_n` — how many samples the corrected distribution actually holds
+- `co_n`: how many samples the corrected distribution holds
(`>= n`, since correction only ever adds synthetic samples).
-- `checksum` — a running sum of a value derived from every call's result
+- `checksum`: a running sum of a value derived from every call's result
(the encoded frame's last byte, or the decoded payload's length). Two
purposes: it stops the compiler from proving the loop body's result is
- unused and deleting it, and — because the workload is fixed — it is
+ unused and deleting it, and (because the workload is fixed) it is
**bit-for-bit identical every run and across all three languages**
(`encode` above: `00000000000fb388` in C++, Go, *and* Rust, for the exact
same input). If the codec is behaving identically everywhere, the
checksums must match; a diverging checksum is a bug, not noise.
-The `min`/`median`/`p99`/`max` numbers above are one machine's numbers — like
+The `min`/`median`/`p99`/`max` numbers above are one machine's numbers: like
every other latency figure in this book, their magnitude is run- and
-hardware-dependent. What's invariant, and what `verify.lua` actually checks,
+hardware-dependent. What's invariant, and what `verify.lua` checks,
is the *shape*: `min <= median <= p99 <= max`, `co_p99 >= p99`, `co_n >= n`,
and a stable checksum across repeated runs of the same workload.
@@ -63,8 +64,8 @@ and a stable checksum across repeated runs of the same workload.
This harness is a **closed loop**: iteration *N+1* never starts until
iteration *N* returns. That's also the classic setup for **coordinated
-omission** (Gil Tene's term): if iteration *N* stalls — a page fault, a
-context switch, a GC-adjacent pause, a neighbor process getting the core —
+omission** (Gil Tene's term): if iteration *N* stalls (a page fault, a
+context switch, a GC-adjacent pause, a neighbor process getting the core),
the *one* sample recorded for it is that stall's full latency, but a
fixed-rate caller (a real client sending one request every `E` nanoseconds)
would have had several requests queue up behind that stall, each observing a
@@ -76,9 +77,9 @@ The correction (HdrHistogram's `recordValueWithExpectedInterval`): for every
raw sample `v` above the expected interval `E`, backfill synthetic samples
`v - E, v - 2E, ...` down to (but not below) `E`, representing the virtual
requests that would have been queued behind the stall. `E` here is set to the
-run's own raw median — i.e., "assume this loop could sustain its typical
-per-op rate" — so no extra flag is needed and the two numbers come from one
-run. A safety cap (`co_n <= 5,000,000`) stops a pathological single giant
+run's own raw median (in other words, "assume this loop could sustain its
+typical per-op rate"), so no extra flag is needed and the two numbers come
+from one run. A safety cap (`co_n <= 5,000,000`) stops a pathological single giant
stall from producing an unbounded backfill.
`co_p99_ns` in the example above (`914420`) vs. the raw `p99_ns` (`124`) is
@@ -87,7 +88,7 @@ to the raw p99 (only 1000 of 100000 samples count toward it) but dominates the
corrected one, because that single stall represented what would have been
thousands of queued requests in a real fixed-rate system.
-### `--lie` — the benchmark this chapter argues against
+### `--lie`: the benchmark this chapter argues against
```
$ ./demo.sh cpp run --lie --op roundtrip
@@ -95,15 +96,15 @@ benchlab: lie op=roundtrip (no warmup, single wall-clock sample, ignores varianc
benchlab: lie elapsed_ns=3069 sink=76
```
-No warmup, one call, one wall-clock delta, no percentiles, no variance — and
-therefore no way to tell a JIT/allocator-cold first call, a scheduler
-preemption, or a page fault apart from steady-state performance. This is not
-a strawman: it is the shape of `time single_call()` benchmarks that get
-pasted into commit messages. `--lie`'s output never contains `p99_ns=` or
-`median_ns=` — there is nothing here to compute a percentile *from* — and
+No warmup, one call, one wall-clock delta, no percentiles, no variance:
+there's no way to tell a JIT/allocator-cold first call, a scheduler
+preemption, or a page fault apart from steady-state performance.
+`time single_call()` benchmarks with this shape get pasted into commit
+messages routinely. `--lie`'s output never contains `p99_ns=` or
+`median_ns=`: there is nothing here to compute a percentile *from*, and
`verify.lua` asserts that absence directly.
-## Wire format (the codec under test, unchanged since ch21)
+## Wire format (the codec under test, unchanged since the TCP chatterd server introduced it)
```
byte: 0 1 2 3 4 5 6 ............. 6+LEN-1
@@ -114,11 +115,12 @@ pasted into commit messages. `--lie`'s output never contains `p99_ns=` or
```
`benchlab` re-implements only `encode_frame`/`decode_frame` against this
-format (a `DELIVER`, type `0x03`) — not the daemon; see ch21/22/27 for the
-networked chatterd that actually speaks this frame over a socket. `decode`
+format (a `DELIVER`, type `0x03`), not the daemon. See the chatterd servers
+elsewhere in the book for the networked chatterd that speaks this frame over
+a socket. `decode`
and `roundtrip` use `std::expected` (C++),
`Result<(u8, Vec), String>` with `?` (Rust), and a wrapped `error` via
-`%w` (Go) to report a malformed frame — on this fixed, always-valid workload
+`%w` (Go) to report a malformed frame. On this fixed, always-valid workload
that path is never taken, but it exists because a hand-rolled codec harness
should look exactly like calling code would, not a stripped-down happy path.
@@ -136,22 +138,22 @@ should look exactly like calling code would, not a stripped-down happy path.
Every language uses a small hand-rolled harness rather than a benchmarking
framework (Google Benchmark / `go test -bench` / criterion): `benchlab` is a
CLI that prints one shared, cross-language table shape, which a framework's
-own report format would not give us for free, and there is no third-party
-dependency to pin or fetch for any of the three languages — `rust/Cargo.lock`
+own report format would not produce, and there is no third-party
+dependency to pin or fetch for any of the three languages: `rust/Cargo.lock`
carries no external crates.
## The demo contract
Every language directory has a `demo.sh` with the book-wide interface:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
(e.g. `./demo.sh run --op decode --iters 100000 --warmup 5000`)
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh` instead of running locally.
-This is a host-only example — pure in-process computation, no sockets, no VM.
+This is a host-only example: pure in-process computation, no sockets, no VM.
Exit codes: `0` a run completed and printed its table (or its lie), `1` the
codec broke on its own fixed workload (a bug, never expected here), `2` a
usage error (missing/unknown flags, or `--warmup >= --iters`).
@@ -168,7 +170,7 @@ time and asserts, per language:
(the run's own raw median, floored at 1); and a 64-bit hex checksum is
printed;
2. running the identical fixed workload twice yields the identical checksum
- both times — the codec's output is deterministic even though the ns
+ both times: the codec's output is deterministic even though the ns
timings are not;
3. `--lie` exits 0, identifies itself as the lie, reports exactly one
`elapsed_ns` sample, and contains **no** `p99_ns=`/`median_ns=` anywhere
diff --git a/examples/40-low-latency-fastpath/README.md b/examples/40-low-latency-fastpath/README.md
index 39b523c..6c41638 100644
--- a/examples/40-low-latency-fastpath/README.md
+++ b/examples/40-low-latency-fastpath/README.md
@@ -1,7 +1,7 @@
-# _template — hello-syscall
+# _template: hello-syscall
The seed for every runnable example in the book. It implements the same tiny
-program three times — C++23, Go, and Rust — each printing exactly one line:
+program three times, in C++23, Go, and Rust, each printing exactly one line:
```
pid on ()
@@ -26,9 +26,9 @@ _template/
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
diff --git a/examples/41-capstone-fleet/README.md b/examples/41-capstone-fleet/README.md
index 39b523c..6c41638 100644
--- a/examples/41-capstone-fleet/README.md
+++ b/examples/41-capstone-fleet/README.md
@@ -1,7 +1,7 @@
-# _template — hello-syscall
+# _template: hello-syscall
The seed for every runnable example in the book. It implements the same tiny
-program three times — C++23, Go, and Rust — each printing exactly one line:
+program three times, in C++23, Go, and Rust, each printing exactly one line:
```
pid on ()
@@ -26,9 +26,9 @@ _template/
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
diff --git a/examples/42-embedded-lua-scripting/README.md b/examples/42-embedded-lua-scripting/README.md
index 64caa9e..458c3b6 100644
--- a/examples/42-embedded-lua-scripting/README.md
+++ b/examples/42-embedded-lua-scripting/README.md
@@ -1,7 +1,7 @@
-# 42 — embedded-lua-scripting
+# 42: embedded-lua-scripting
`pmon` lifts its restart/backoff decision out of the host binary and into a
-shared **`policy.lua`**. Each host — C++ (sol2), Go (gopher-lua), Rust (mlua) —
+shared **`policy.lua`**. Each host, C++ (sol2), Go (gopher-lua), Rust (mlua),
embeds a Lua interpreter, sandboxes it, and calls `on_exit(info)` after each
supervised child dies. Lua returns a `{ action, delay_ms, reason }` decision the
host obeys. Edit `policy.lua`, send **SIGHUP**, and the running supervisor
@@ -24,16 +24,16 @@ policy --host.now_ms()--> host -- monotonic-ish clock, for time-aware policies
The sandbox opens only `base`, `string`, `table`, `math`, and removes
`load`/`loadfile`/`dofile`/`require`; `os`, `io`, `package` and `debug` are
never loaded. A policy therefore cannot touch the filesystem, spawn processes,
-or pull in new code — `policy-evil.lua` proves it.
+or pull in new code; `policy-evil.lua` proves it.
## Policy files
| file | role |
|---|---|
-| `policy.lua` | v1 — base backoff 500ms, give up after 8 restarts |
-| `policy-v2.lua` | v2 — base backoff 100ms, give up after 2 restarts (the SIGHUP edit) |
-| `policy-evil.lua` | calls `os.execute` — rejected by the sandbox at runtime |
-| `policy-broken.lua` | a syntax error — rejected at load |
+| `policy.lua` | v1: base backoff 500ms, give up after 8 restarts |
+| `policy-v2.lua` | v2: base backoff 100ms, give up after 2 restarts (the SIGHUP edit) |
+| `policy-evil.lua` | calls `os.execute`: rejected by the sandbox at runtime |
+| `policy-broken.lua` | a syntax error: rejected at load |
`policy.lua` uses only the intersection of Lua 5.1 (gopher-lua) and Lua 5.4
(sol2/mlua): no `//`, no bitwise operators, no ``/``, no
diff --git a/examples/43-rust-macros-for-systems/README.md b/examples/43-rust-macros-for-systems/README.md
index 95816ad..cf48fc1 100644
--- a/examples/43-rust-macros-for-systems/README.md
+++ b/examples/43-rust-macros-for-systems/README.md
@@ -1,17 +1,17 @@
-# 43 — rust-macros-for-systems
+# 43: rust-macros-for-systems
The book's first **single-language** example (Rust only): the deep dives don't
mirror across three languages. It shows three metaprogramming techniques a
-systems programmer actually reaches for, as a small cargo **workspace** — a
+systems programmer reaches for, as a small cargo **workspace**: a
proc-macro crate plus the binary that uses it:
-- **`#[derive(SysError)]`** — a derive macro that generates `Display` + `std::error::Error`
+- **`#[derive(SysError)]`**: a derive macro that generates `Display` + `std::error::Error`
from `#[error("...")]` attributes (a miniature `thiserror`), for errno-style
error enums.
-- **`#[instrument]`** — an attribute macro that wraps a function to log its
- entry (name + args), exit (return value), and wall-clock duration — tracing
+- **`#[instrument]`**: an attribute macro that wraps a function to log its
+ entry (name + args), exit (return value), and wall-clock duration, tracing
without a runtime.
-- **A sound `unsafe` unaligned read**, validated with **miri** — plus a
+- **A sound `unsafe` unaligned read**, validated with **miri**, plus a
defect-named `--oob-read` path that miri flags as undefined behaviour.
## Layout
@@ -38,7 +38,7 @@ proc-macro crate plus the binary that uses it:
## Verify
`verify.lua` (Rust only; skips other langs) asserts the generated `Display`
-strings, the `#[instrument]` entry/exit/timing lines, and the parsed record —
+strings, the `#[instrument]` entry/exit/timing lines, and the parsed record:
behaviour, not exit codes. The gate runs on the **pinned stable** toolchain;
miri (nightly) is exercised by the chapter, not the gate.
diff --git a/examples/44-go-runtime-for-systems/README.md b/examples/44-go-runtime-for-systems/README.md
index 163468d..40b0f09 100644
--- a/examples/44-go-runtime-for-systems/README.md
+++ b/examples/44-go-runtime-for-systems/README.md
@@ -1,23 +1,23 @@
-# 44 — go-runtime-for-systems
+# 44: go-runtime-for-systems
A **single-language** example (Go only): a program that observes its own Go
runtime across four themes, printing self-computed verdict lines so the
behaviour is checkable, not just describable.
-- **`sched`** — the GMP scheduler: a rendezvous barrier proves N goroutines are
+- **`sched`**: the GMP scheduler: a rendezvous barrier proves N goroutines are
concurrently alive, a worker pool proves work distribution, and a busy-loop
under `GOMAXPROCS=1` proves signal-based **async preemption** (a starved
goroutine still makes progress).
-- **`gc`** — GC pacing: with auto-GC off, one `runtime.GC()` advances `NumGC` by
+- **`gc`**: GC pacing: with auto-GC off, one `runtime.GC()` advances `NumGC` by
exactly 1; then the same churn under `GOGC=100` vs `800`, and under a tight
`GOMEMLIMIT`, shows the pacer's CPU/memory trade-off in collection counts.
-- **`netpoll`** — 2000 goroutines block on pipe reads while the OS **thread**
+- **`netpoll`**: 2000 goroutines block on pipe reads while the OS **thread**
count stays bounded (`<= 4*GOMAXPROCS+16`): goroutines are not threads.
-- **`knobs`** — echoes the effective `GOMAXPROCS`/`NumCPU`/`GOGC`/`GOMEMLIMIT`/
+- **`knobs`**: echoes the effective `GOMAXPROCS`/`NumCPU`/`GOGC`/`GOMEMLIMIT`/
`GODEBUG`.
On **Go 1.26** this runs with the Green Tea GC on by default and container-aware
-`GOMAXPROCS` (both since 1.25/1.26) — see the chapter.
+`GOMAXPROCS` (both since 1.25/1.26); see the chapter.
## Run it
@@ -37,7 +37,7 @@ GODEBUG=asyncpreemptoff=1 ./go/bin/app sched # the preempt check flips to FAIL
## Verify
`verify.lua` (Go only; skips other langs) matches each theme's fixed verdict
-label ending in the literal `ok`, plus the `ALL: PASS` roll-up — behaviour the
+label ending in the literal `ok`, plus the `ALL: PASS` roll-up: behaviour the
program asserts about itself, run on the pinned Go 1.26.5 toolchain.
```bash
@@ -45,4 +45,4 @@ LSP_LANG=go lua verify.lua
```
Mode: `local`. Stdlib only (`runtime`, `runtime/debug`, `os`, `sync`,
-`sync/atomic`, `bufio`) — no external modules.
+`sync/atomic`, `bufio`); no external modules.
diff --git a/examples/45-linux-analysis-suites/README.md b/examples/45-linux-analysis-suites/README.md
index c3683bd..424e9ea 100644
--- a/examples/45-linux-analysis-suites/README.md
+++ b/examples/45-linux-analysis-suites/README.md
@@ -1,4 +1,4 @@
-# 45 — linux analysis suites (PCP)
+# 45: linux analysis suites (PCP)
A single Fedora-based, multi-stage **Containerfile** that builds `pmcd`
(Performance Co-Pilot's collection daemon) with two PMDAs (Performance
@@ -7,7 +7,7 @@ Built and run with **rootless podman**.
Unlike every other numbered example in this book, there is no C++/Go/Rust
source here. PCP is an OS-level metrics suite you point at things, not a
-program you write in a language — `./demo.sh [cpp|go|rust] [build|run]`
+program you write in a language: `./demo.sh [cpp|go|rust] [build|run]`
accepts the language argument purely so `scripts/test-all-examples.py`'s
per-language loop can drive it; the image built and run is identical
regardless of which language is passed.
@@ -16,19 +16,19 @@ regardless of which language is passed.
This project's other container examples (`examples/34-...`) use UBI10. This
one doesn't: checked directly against a real `dnf install`, `pcp` and its
-PMDAs are **not resolvable on UBI 9/10** without a RHEL entitlement — no
+PMDAs are **not resolvable on UBI 9/10** without a RHEL entitlement: no
free/anonymous UBI repo carries them, and this book's readers won't have a
subscription. Fedora's own repos carry `pcp 7.1.5` and every PMDA package
unauthenticated, so `registry.fedoraproject.org/fedora:44` is the base that
-actually builds for everyone.
+builds for everyone.
-## What's inside, and what's deliberately not
+## What's included, and what isn't
| PMDA | Status | Why |
|---|---|---|
| `pmda-openmetrics` | **In, with a real scraped value** | Bridges an OpenMetrics/Prometheus-style document into PCP's namespace. Pointed at a `file://` source (`config/lsp45.prom`, shipped in this example) rather than an HTTP endpoint, so the scrape is real and reproducible with zero network dependency. |
-| `pmda-podman` | **In, registers always; live values need a socket mount** | Reads podman's container/pod state over its API socket. Registers its full metric namespace (21 metrics) at build time with no socket present. At `podman run` time, `demo.sh` mounts the **host's own rootless** `podman.sock` into the container at `/run/podman/podman.sock`, and the PMDA then reports REAL running containers — verified live, including the demo container seeing itself. (There's a known upstream report, [performancecopilot/pcp#913](https://github.com/performancecopilot/pcp/issues/913), that pmda-podman only sees root-managed containers; that was NOT reproduced here — mounting a rootless user's own socket at the expected path worked directly, at least on PCP 7.1.5/podman 5.8.4.) |
-| `pmda-bcc` | **Excluded entirely** | Needs a working BCC/eBPF stack, which needs a super-privileged container (raw BPF syscalls, `/sys/kernel/debug`, a matching `kernel-devel`) that cannot load rootless — the opposite of everything else in this example. It's covered in the chapter as PCP's privileged sibling PMDA, demonstrated on the `systems-target` VM, not shipped in this image. This matches the project's "eBPF is tooling only" scope rule. |
+| `pmda-podman` | **In, registers always; live values need a socket mount** | Reads podman's container/pod state over its API socket. Registers its full metric namespace (21 metrics) at build time with no socket present. At `podman run` time, `demo.sh` mounts the **host's own rootless** `podman.sock` into the container at `/run/podman/podman.sock`, and the PMDA then reports *real* running containers, verified live, including the demo container seeing itself. (There's a known upstream report, [performancecopilot/pcp#913](https://github.com/performancecopilot/pcp/issues/913), that pmda-podman only sees root-managed containers; that was *not* reproduced here: mounting a rootless user's own socket at the expected path worked directly, at least on PCP 7.1.5/podman 5.8.4.) |
+| `pmda-bcc` | **Excluded entirely** | Needs a working BCC/eBPF stack, which needs a super-privileged container (raw BPF syscalls, `/sys/kernel/debug`, a matching `kernel-devel`) that cannot load rootless, the opposite of everything else in this example. It's covered in the chapter as PCP's privileged sibling PMDA, demonstrated on the `systems-target` VM, not shipped in this image. This matches the project's "eBPF is tooling only" scope rule. |
## Layout
@@ -47,18 +47,18 @@ actually builds for everyone.
| Stage | Does |
|---|---|
| `builder` | `dnf install pcp pcp-system-tools pcp-pmda-openmetrics pcp-pmda-podman`; ships `config/lsp45.prom`; points `openmetrics`'s `config.d` at it via a `file://` source; starts `pmcd` **inside the build** and activates both PMDAs against that live `pmcd`. |
-| `smoke` | Starts `pmcd` again from the config the builder stage just wrote, confirms **both** PMDAs registered in `pcp`'s summary, and confirms **real metric values** come back — the scraped `openmetrics.lsp45.lsp45_answer` (`42`) and the core `kernel.all.load`. If either PMDA silently failed to attach, this stage — and therefore the whole `podman build` — fails. |
-| `runtime` | `FROM smoke` (not `FROM builder`) — chaining onto `smoke` is what makes podman actually build that stage; an unreferenced intermediate stage with no `COPY --from`/`FROM` edge into it is silently skipped by `podman build`, which would turn the "build-time gate" into a no-op. Ships the smoke stage's already-activated PMNS/`pmcd.conf`. `ENTRYPOINT ["/usr/libexec/pcp/bin/pmcd", "-f"]`. |
+| `smoke` | Starts `pmcd` again from the config the builder stage just wrote, confirms **both** PMDAs registered in `pcp`'s summary, and confirms **real metric values** come back: the scraped `openmetrics.lsp45.lsp45_answer` (`42`) and the core `kernel.all.load`. If either PMDA silently fails to attach, this stage (and therefore the whole `podman build`) fails. |
+| `runtime` | `FROM smoke` (not `FROM builder`): chaining onto `smoke` is what makes podman build that stage; an unreferenced intermediate stage with no `COPY --from`/`FROM` edge into it is silently skipped by `podman build`, which would turn the "build-time gate" into a no-op. Ships the smoke stage's already-activated PMNS/`pmcd.conf`. `ENTRYPOINT ["/usr/libexec/pcp/bin/pmcd", "-f"]`. |
-## The one real build gotcha
+## A build-ordering requirement
Each PMDA's `./Install` script (`pmdaproc.sh`) tries to restart `pmcd` via
-`systemctl restart pmcd.service` whenever it thinks a restart — rather than
-a lighter SIGHUP — is needed. This build has no systemd, so that call fails,
+`systemctl restart pmcd.service` whenever it thinks a restart (rather than
+a lighter SIGHUP) is needed. This build has no systemd, so that call fails,
and Install's failure path **reverts its own `pmcd.conf` edit**, silently
dropping the PMDA it just tried to add. The fix, visible in the
Containerfile: start `pmcd` **before** either `./Install` call. `pmdaproc.sh`
-has a cheaper path — if `pminfo -v pmcd.version` already succeeds against a
+has a cheaper path: if `pminfo -v pmcd.version` already succeeds against a
running `pmcd`, it just sends `pmsignal -a -s HUP pmcd` instead of trying a
full restart, and that path needs no systemd at all. Installing against an
already-running `pmcd` takes that path for both PMDAs and nothing gets
@@ -74,7 +74,7 @@ reverted.
`run` mounts the host's own rootless `podman.sock` into the container (if
one is active for the invoking user) so the podman PMDA reports live
container state, not just an empty registered namespace. If no socket is
-found, it says so and continues — the podman PMDA still registers, it just
+found, it says so and continues: the podman PMDA still registers, it just
reports zero values.
```bash
@@ -94,20 +94,20 @@ podman rm -f lsp45-pcp-demo; podman rmi -f lsp45-pcp
`verify.lua` requires podman on the host (`skip()`, exit 77, if missing) and
runs only under `LSP_LANG=cpp` (the manifest lists `langs: [cpp]` for this
-example, since there's no per-language behavior to triple-run). It actually
-builds the image and drives a real container:
+example, since there's no per-language behavior to triple-run). It builds
+the image and drives a real container:
-- the build succeeds — meaningful because the Containerfile's own smoke
+- the build succeeds, which matters because the Containerfile's own smoke
stage already gates this: a PMDA that fails to register fails the build,
- not just the demo, confirmed by deliberately breaking it once during
+ not just the demo, confirmed by breaking it once during
development (starting `pmcd` *after* the `./Install` calls) and watching
the build fail with the exact `pmcd.conf` revert described above;
- `pmcd` answers `pminfo` within a bounded 20s poll;
- `pcp`'s own summary lists both `openmetrics` and `podman` as active PMDAs;
-- `pminfo -f openmetrics.lsp45.lsp45_answer` returns `value 42` — a real
+- `pminfo -f openmetrics.lsp45.lsp45_answer` returns `value 42`: a real
parse of the real file this example ships, every run;
- the podman PMDA's namespace registers (`podman.container.*` metrics
- exist) — this script does not gate on live container *values*, since that
+ exist); this script does not gate on live container *values*, since that
needs a mounted socket not guaranteed on every host/CI environment; the
live-value behavior is real and demonstrated by `demo.sh`, just not
asserted here;
@@ -125,22 +125,22 @@ PASS 10 / FAIL 0
Image size on that host: **265 MB** (`podman images lsp45-pcp`). This is
much larger than `examples/34-our-programs-in-containers`'s few-tens-of-MB
-runtime images — PCP's own dependency chain (Python, `pcp-libs`,
+runtime images: PCP's own dependency chain (Python, `pcp-libs`,
`pcp-system-tools`) is the reason; there's no further multi-stage trim
available, since the shipped `runtime` stage needs the exact same `pmcd`/
PMDA/Python userspace the `smoke` stage just proved works.
-## A cosmetic quirk, noted rather than hidden
+## A cosmetic leftover PMNS entry
`pminfo openmetrics` (no `-f`) additionally lists
`openmetrics.grafana.go_memstats_last_gc_since_start_time_seconds` even
though this image's `config.d` only ever contains `lsp45.url`. Fetching it
-(`pminfo -f openmetrics.grafana...`) always returns "No value(s) available!"
-— it's an inert, unexplained leftover PMNS entry, not a real metric source,
+(`pminfo -f openmetrics.grafana...`) always returns "No value(s) available!":
+it's an inert, unexplained leftover PMNS entry, not a real metric source,
and it does not affect any assertion this example makes (which targets the
specific `openmetrics.lsp45.lsp45_answer` name by exact match). Tracing it
-further — it's not in the PMDA's on-disk instance-cache pickle
+further went past the point of diminishing returns for a chapter appendix:
+it's not in the PMDA's on-disk instance-cache pickle
(`/var/lib/pcp/config/pmda/144.*.py`), which only ever contains `control`
-and `lsp45` — went past the point of diminishing returns for a chapter
-appendix; flagged here rather than silently ignored, per this project's
-verification-discipline rule.
+and `lsp45`. It's flagged here rather than silently ignored, per this
+project's verification-discipline rule.
diff --git a/examples/46-cpp-toolbox/README.md b/examples/46-cpp-toolbox/README.md
index aa02156..1151339 100644
--- a/examples/46-cpp-toolbox/README.md
+++ b/examples/46-cpp-toolbox/README.md
@@ -1,17 +1,17 @@
-# 46 — cpp-toolbox
+# 46: cpp-toolbox
A **single-language** example (C++ only): the C++ toolchain itself is the
subject, so a Go or Rust variant would have nothing to show. One `toolbox`
binary plus a full set of toolchain artifacts, engineered so every tool
produces a checkable effect, not just a clean exit.
-- **`toolbox report`** — an FNV-1a digest over a fixed embedded byte array,
+- **`toolbox report`**: an FNV-1a digest over a fixed embedded byte array,
printed via a custom `Digest { uint64_t fnv; }` type. Integer/string
output only (no floats, addresses, timing, or unordered-container
iteration), so a GCC build and a clang build print byte-identical output.
-- **`toolbox defect overflow`** — deliberately triggers signed-integer
- overflow, caught by the `ubsan` CMake preset.
-- `src/smell.cpp` — one deliberate, behavior-preserving clang-tidy smell
+- **`toolbox defect overflow`**: triggers signed-integer overflow, caught by
+ the `ubsan` CMake preset.
+- `src/smell.cpp`: a seeded, behavior-preserving clang-tidy smell
(`performance-unnecessary-value-param`), compiled into the same binary so
clang-tidy sees it through the compile-commands database.
@@ -44,15 +44,15 @@ produces a checkable effect, not just a clean exit.
## The demo contract
-- `./demo.sh cpp` — build then run (`release` preset)
-- `./demo.sh cpp build` — build only
-- `./demo.sh cpp run [args]` — run the built binary
+- `./demo.sh cpp`: build then run (`release` preset)
+- `./demo.sh cpp build`: build only
+- `./demo.sh cpp run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
The default build (`release` preset, what `./demo.sh cpp build` and CI
-build-smoke use) never needs Conan, clang-tidy, clang-format, or gdb -- those
+build-smoke use) never needs Conan, clang-tidy, clang-format, or gdb: those
are exercised directly by `verify.lua` and the chapter's hand-run recipes.
## Verify
@@ -71,7 +71,7 @@ Hard-gated, always asserted:
`Digest(0x...)`.
Gated only if the tool is present on `PATH` (never fails the example if
-absent -- degrades to an informational `SKIP:` line):
+absent: degrades to an informational `SKIP:` line):
- **D.** clang-tidy flags `performance-unnecessary-value-param` at
`src/smell.cpp:3`.
@@ -81,7 +81,7 @@ absent -- degrades to an informational `SKIP:` line):
preset build + run prints the expected line.
ccache and mold are checked opportunistically and printed for information
-only -- both are expected absent on the reference host and are never part
+only; both are expected absent on the reference host and are never part
of the pass/fail count.
Mode: `local`. No root, no VM, no LGTM stack.
diff --git a/examples/47-go-toolbox/README.md b/examples/47-go-toolbox/README.md
index 80c719c..8308857 100644
--- a/examples/47-go-toolbox/README.md
+++ b/examples/47-go-toolbox/README.md
@@ -1,50 +1,51 @@
-# 47 — go-toolbox
+# 47: go-toolbox
A **single-language** example (Go only): the Go toolchain itself is the
-subject, mirroring ch46's C++ toolbox. Every tool the chapter names asserts a
-real, tool-produced effect against this module — never a bare exit code.
+subject, mirroring the C++ toolbox chapter. Every tool the chapter names
+asserts a real, tool-produced effect against this module, never a bare exit
+code.
-- **`toolbox report`** — a deterministic FNV-1a digest over a fixed embedded
+- **`toolbox report`**: a deterministic FNV-1a digest over a fixed embedded
16-byte payload, integer/string output only (no floats, addresses, timing,
- or map iteration). The payload is byte-for-byte ch46's C++ payload, so both
- chapters land on the identical digest `0x481984990deee5ff` — a
- cross-appendix easter egg, not a coincidence.
-- **`toolbox tools`** — prints `tools_generated.go`'s sorted table, proof
+ or map iteration). The payload is byte-for-byte the C++ toolbox's payload,
+ so both chapters land on the identical digest `0x481984990deee5ff`, a
+ deliberate cross-appendix match.
+- **`toolbox tools`**: prints `tools_generated.go`'s sorted table, proof
that `go generate` produced real, checkable output.
-- **`toolbox defect divzero`** — deliberately panics with a runtime integer
- divide-by-zero (narrative parity with ch46's UBSan-caught overflow).
-- **`go generate`** (`internal/gen`, stdlib-only, `go/format`-clean) —
+- **`toolbox defect divzero`**: panics with a runtime integer divide-by-zero
+ (narrative parity with the C++ toolbox's UBSan-caught overflow).
+- **`go generate`** (`internal/gen`, stdlib-only, `go/format`-clean):
regenerates `tools_generated.go` byte-identical to the committed copy.
-- **`gofmt`** vs **`gofumpt`** — `testdata/gofmt/messy.go` is gofmt-dirty;
+- **`gofmt`** vs **`gofumpt`**: `testdata/gofmt/messy.go` is gofmt-dirty;
`testdata/gofumpt/gofmt_ok.go` is gofmt-clean but gofumpt-dirty (a leading
blank line inside a block).
-- **`go vet -tags vetdemo .`** — `vetdemo.go` (excluded from the default
- build by its build tag) has a deliberate `Printf` format-verb mismatch.
-- **`go tool pprof`** — `bench_test.go`'s `hotLoop` is a deliberately
- dominant hot function; the gate asserts pprof names it, never a timing.
-- **`golangci-lint`** / **`staticcheck`** — `smells.go` has unused-function
+- **`go vet -tags vetdemo .`**: `vetdemo.go` (excluded from the default
+ build by its build tag) has a seeded `Printf` format-verb mismatch.
+- **`go tool pprof`**: `bench_test.go`'s `hotLoop` is a hot function built
+ to dominate the profile; the gate asserts pprof names it, never a timing.
+- **`golangci-lint`** / **`staticcheck`**: `smells.go` has unused-function
(`U1000`), dead-store (`SA4006`), and naming (`ST1003`) smells, never
called at runtime so they cannot affect the digest.
-- **`delve`** — `go/.dlvinit` is a committed headless-batch script: break,
+- **`delve`**: `go/.dlvinit` is a committed headless-batch script: break,
continue, print the digest field, quit.
-- **`benchstat`** — `bench_test.go` also has a `b.RunParallel` benchmark for
+- **`benchstat`**: `bench_test.go` also has a `b.RunParallel` benchmark for
a `-cpu=1` vs `-cpu=N` comparison (concurrency lens).
## Host reality: hard core only, offline
This example was built and verified on a host with **only `go` (1.26.5)
-present** — `gofumpt`, `golangci-lint`, `staticcheck`, `govulncheck`, `dlv`,
-and `benchstat` are all absent, and per this iteration's decision, none are
-installed (no network, no `go install`). The main module is **stdlib-only**
-(no `go.sum`) and builds fully offline.
+present**: `gofumpt`, `golangci-lint`, `staticcheck`, `govulncheck`, `dlv`,
+and `benchstat` are all absent, and none are installed (no network, no
+`go install`). The main module is **stdlib-only** (no `go.sum`) and builds
+fully offline.
Gates A-E below are go-native and always run. Gates F, G, H, I, K
-(gofumpt/golangci-lint/staticcheck/delve/benchstat) degrade to an
+(`gofumpt`/`golangci-lint`/`staticcheck`/`delve`/`benchstat`) degrade to an
informational `SKIP: absent` on this host; a reader with the tools
-installed exercises them for real. There is no `govulncheck` gate at all: it
-needs network access to the Go vulnerability database and a deliberately
-vulnerable dependency, both excluded here — the chapter covers it as
-illustrative prose only, never gated, never committed.
+installed runs them directly. There is no `govulncheck` gate at all: it
+needs network access to the Go vulnerability database and a vulnerable
+dependency, both excluded here: the chapter covers it as illustrative prose
+only, never gated, never committed.
## Run it
@@ -61,13 +62,13 @@ go vet -tags vetdemo ./go/... # the seeded printf finding
## Verify
-`verify.lua` (Go only; skips other langs) asserts gates A-E for real and
+`verify.lua` (Go only; skips other langs) asserts gates A-E directly and
prints an informational `SKIP:` for any of F, G, H, I, K whose tool is
-absent from `PATH` — it never hard-fails or aborts on a missing optional
+absent from `PATH`: it never hard-fails or aborts on a missing optional
tool.
```bash
LSP_LANG=go REPO_ROOT=$(cd ../.. && pwd) lua verify.lua
```
-Mode: `local`. Stdlib only — no external modules, no `go.sum`.
+Mode: `local`. Stdlib only: no external modules, no `go.sum`.
diff --git a/examples/48-rust-toolbox/README.md b/examples/48-rust-toolbox/README.md
index 6758275..75350c0 100644
--- a/examples/48-rust-toolbox/README.md
+++ b/examples/48-rust-toolbox/README.md
@@ -1,57 +1,57 @@
-# 48 — rust-toolbox
+# 48: rust-toolbox
A **single-language** example (Rust only): the Rust toolchain itself is the
-subject, closing the three-chapter arc that ch46 (C++) and ch47 (Go) opened.
-Every tool the chapter names asserts a real, tool-produced effect against
-this crate — never a bare exit code.
+subject, closing the three-chapter arc that the C++ and Go toolbox chapters
+opened. Every tool the chapter names asserts a real, tool-produced effect
+against this crate, never a bare exit code.
-- **`toolbox report`** — a deterministic FNV-1a digest over a fixed embedded
+- **`toolbox report`**: a deterministic FNV-1a digest over a fixed embedded
16-byte payload, integer/string output only (no floats, addresses, timing,
- or hash-map iteration). The payload is byte-for-byte ch46's C++ and ch47's
- Go payload, so all three chapters land on the identical digest
- `0x481984990deee5ff` — a cross-appendix easter egg, not a coincidence.
-- **`rust-toolchain.toml`** — pins `channel = "1.97.1"` plus the `rustfmt`
+ or hash-map iteration). The payload is byte-for-byte the same as the C++
+ and Go toolbox payloads, so all three chapters land on the identical digest
+ `0x481984990deee5ff`, a deliberate cross-appendix match.
+- **`rust-toolchain.toml`**: pins `channel = "1.97.1"` plus the `rustfmt`
and `clippy` components. The pin travels with the repository: `cargo` in
this directory resolves to 1.97.1 whatever `rustup default` says globally.
-- **`cargo fmt --check`** — clean across the crate's module tree;
+- **`cargo fmt --check`**: clean across the crate's module tree;
`fixtures/misformatted.rs` lives *outside* that tree (nothing `mod`-declares
it) so only an explicit per-file `rustfmt --edition 2024 --check` reads it,
and that must exit nonzero with a diff.
-- **`cargo clippy --all-targets -- -D warnings`** — clean on the default
- build; `--features lintbait` compiles `src/lintbait.rs`'s deliberate
+- **`cargo clippy --all-targets -- -D warnings`**: clean on the default
+ build; `--features lintbait` compiles `src/lintbait.rs`'s seeded
`return y;` and fires the stable `needless_return` lint. The bait is *not*
- `#[allow]`-ed — allowing it would defeat the gate.
-- **`cargo test`** — `src/digest.rs`'s `fnv1a_matches_cross_language_digest`
+ `#[allow]`-ed: allowing it would defeat the gate.
+- **`cargo test`**: `src/digest.rs`'s `fnv1a_matches_cross_language_digest`
asserts the digest literal, so drift in the payload or the hash fails here
before it reaches the report gate.
- **`cargo-nextest`** / **`cargo-deny`** / **`cargo-llvm-cov`** /
- **`cargo-audit`** — gated *if present* against the committed `deny.toml`
+ **`cargo-audit`**: gated *if present* against the committed `deny.toml`
and `clippy.toml`.
-- **`cargo-watch`**, **`cargo-flamegraph`**, **`sccache`** — shown in the
+- **`cargo-watch`**, **`cargo-flamegraph`**, **`sccache`**: shown in the
chapter, never gated (interactive / needs `perf` privileges / a cache-hit
- rate is not a correctness property). **miri** is cross-referenced to ch29
- and ch43, which own it.
+ rate is not a correctness property). **miri** is covered in the sanitizers
+ chapter and the Rust macros chapter, which own it.
-## Host reality: a bigger hard core than ch47, offline
+## Host reality: a bigger hard core than the Go toolbox, offline
This example was built and verified on a host with **only the pinned Rust
-toolchain present** — `cargo-nextest`, `cargo-deny`, `cargo-llvm-cov` and
+toolchain present**: `cargo-nextest`, `cargo-deny`, `cargo-llvm-cov` and
`sccache` are absent, `cargo-audit` is installed but its RustSec advisory
-database is not fetched, and per this iteration's decision none of that is
-installed or fetched (no network, no `cargo install`). The crate is
-**zero-dependency**, so `cargo build --offline` really is offline.
+database is not fetched, and none of that is installed or fetched (no
+network, no `cargo install`). The crate is **zero-dependency**, so
+`cargo build --offline` is offline by construction.
-The interesting asymmetry with ch47: **rustfmt and clippy are rustup
-components of the pinned channel**, so pinning the channel pins the formatter
-and the linter too. Go's `gofumpt` and `golangci-lint` are separate downloads
-and could only ever be gated-if-present. That is why ch48's hard core (A–D)
-covers formatting *and* linting offline, where ch47's could not.
+The asymmetry with Go: **rustfmt and clippy are rustup components of the
+pinned channel**, so pinning the channel pins the formatter and the linter
+too. Go's `gofumpt` and `golangci-lint` are separate downloads and could
+only ever be gated-if-present. That is why this chapter's hard core (A-D)
+covers formatting *and* linting offline, where Go's could not.
-Gates A–D always run. Gates E, F, G, H
+Gates A-D always run. Gates E, F, G, H
(nextest/deny/llvm-cov/audit) degrade to an informational `SKIP: …` on this
-host; a reader who has installed the tools exercises them for real. The
-cargo-audit gate is guarded twice — on the binary *and* on
-`~/.cargo/advisory-db` — so a missing database is a skip, not a failure.
+host; a reader who has installed the tools runs them directly. The
+cargo-audit gate is guarded twice: on the binary *and* on
+`~/.cargo/advisory-db`, so a missing database is a skip, not a failure.
## Run it
@@ -69,14 +69,14 @@ cargo test --offline # the digest test
## Verify
-`verify.lua` (Rust only; skips other langs) asserts gates A–D for real and
-prints an informational `SKIP:` for any of E, F, G, H whose tool — or, for
-cargo-audit, whose advisory database — is absent. It never hard-fails or
+`verify.lua` (Rust only; skips other langs) asserts gates A-D directly and
+prints an informational `SKIP:` for any of E, F, G, H whose tool (or, for
+cargo-audit, whose advisory database) is absent. It never hard-fails or
aborts on a missing optional tool.
```bash
LSP_LANG=rust REPO_ROOT=$(cd ../.. && pwd) lua verify.lua
```
-Mode: `local`. Zero-dependency — the `Cargo.lock` contains exactly one
+Mode: `local`. Zero-dependency: the `Cargo.lock` contains exactly one
package, `toolbox` itself.
diff --git a/examples/49-concurrency-vs-parallelism/README.md b/examples/49-concurrency-vs-parallelism/README.md
index 832758d..c641219 100644
--- a/examples/49-concurrency-vs-parallelism/README.md
+++ b/examples/49-concurrency-vs-parallelism/README.md
@@ -1,4 +1,4 @@
-# 49 — concurrency vs parallelism
+# 49: concurrency vs parallelism
The framing example for Part 14, the C++ concurrency compendium. It runs **one
workload under four execution models** and reports observables that describe
@@ -8,27 +8,27 @@ workload under four execution models** and reports observables that describe
conc report: model=concurrent workers=16 cpus_allowed=1 distinct_cpus=1 max_inflight=16 interleaves=19 consensus=yes digest=0x481984990deee5ff
```
-Concurrency is **structure** — how many tasks are in flight. Parallelism is
-**execution** — how many CPUs are occupied. They are independent axes, which is
+Concurrency is **structure**: how many tasks are in flight. Parallelism is
+**execution**: how many CPUs are occupied. They are independent axes, which is
why all four cells below exist.
| `--model` | workers | what it asks the machine for | reading |
| --- | --- | --- | --- |
| `sequential` | 4 | one task at a time, on this thread, no threads spawned | neither concurrent nor parallel |
| `concurrent` | one per CPU | N runnable threads, pinned to CPU 0 by `sched_setaffinity(2)` | **concurrent, not parallel** |
-| `parallel` | one per CPU | N threads, unpinned — tasks need not share a CPU | parallel, minimal task sharing |
-| `both` | two per CPU | 2N threads, unpinned — oversubscribed | **concurrent and parallel** |
+| `parallel` | one per CPU | N threads, unpinned (tasks need not share a CPU) | parallel, minimal task sharing |
+| `both` | two per CPU | 2N threads, unpinned (oversubscribed) | **concurrent and parallel** |
## The observables
Nothing here is a duration. Wall-clock numbers are not reproducible across
-hosts or runs and this book does not gate on them (see ch39); every field below
-is a count or a set cardinality, which are.
+hosts or runs and this book does not gate on them (see the benchmarking
+chapter); every field below is a count or a set cardinality, which are.
| field | how it is obtained | what it means |
| --- | --- | --- |
-| `cpus_allowed` | `CPU_COUNT` over `sched_getaffinity(2)` | a **permission** — how many CPUs the process may use |
-| `distinct_cpus` | popcount of a bitmap unioned from `sched_getcpu()` at every sample point | an **outcome** — how many CPUs workers were actually seen on |
+| `cpus_allowed` | `CPU_COUNT` over `sched_getaffinity(2)` | a **permission**: how many CPUs the process may use |
+| `distinct_cpus` | popcount of a bitmap unioned from `sched_getcpu()` at every sample point | an **outcome**: how many CPUs workers were seen on |
| `max_inflight` | high-water mark of the live-worker counter | tasks in flight |
| `interleaves` | gaps in each worker's run of monotonic tickets | observed handoffs between workers |
| `digest` | FNV-1a over the 16 bytes `The quick brown.` | the answer, which must not depend on the model |
@@ -37,8 +37,8 @@ is a count or a set cardinality, which are.
`cpus_allowed` versus `distinct_cpus` is the distinction the whole chapter turns
on: being permitted 16 CPUs is not the same as occupying them.
-The digest is `0x481984990deee5ff` — deliberately the same value ch46's C++,
-ch47's Go, and ch48's Rust toolbox land on, over the same payload bytes.
+The digest is `0x481984990deee5ff`: the same value the C++, Go, and Rust
+toolbox chapters land on, by design, over the same payload bytes.
## Layout
@@ -56,13 +56,13 @@ ch47's Go, and ch48's Rust toolbox land on, over the same payload bytes.
└── hardening/oob.cpp # built twice: checked and unchecked
```
-This is a **C++-only example** — the first in the book. A Go or Rust variant
+This is a **C++-only example**: the first in the book. A Go or Rust variant
would answer a different question: each of those languages has one blessed
concurrency model, where the whole point here is that C++ offers several and
you have to choose. `verify.lua` skips for any `LSP_LANG` other than `cpp`.
-Stdlib and Linux scheduler syscalls only. **No Boost, no Conan, no network** —
-ch50 onward add the libraries.
+Stdlib and Linux scheduler syscalls only. **No Boost, no Conan, no network**:
+later chapters add the libraries.
## Build and run
@@ -76,7 +76,7 @@ ch50 onward add the libraries.
Bare `./demo.sh` builds and runs `--model both`. `TARGET=` deploys the
binary to a lab VM via `scripts/lab/deploy-to-vm.sh`, though nothing here needs
-one — the demo runs locally and unprivileged.
+one: the demo runs locally and unprivileged.
The two side programs are separate binaries so a trapping run can never take
the four-model demo with it:
@@ -95,30 +95,30 @@ the four-model demo with it:
Hard gates, all offline:
-- **A** — all four models produce the identical digest and `consensus=yes`.
-- **B** — `parallel` occupied more than one CPU. This is the one
+- **A**: all four models produce the identical digest and `consensus=yes`.
+- **B**: `parallel` occupied more than one CPU. This is the one
scheduler-dependent assertion, so it retries a bounded number of times: "at
least one of K runs saw more than one CPU" is a real effect, where a single
run on a saturated host could legitimately see one.
-- **C** — `concurrent` reports `cpus_allowed=1` *and* `distinct_cpus=1` *and*
+- **C**: `concurrent` reports `cpus_allowed=1` *and* `distinct_cpus=1` *and*
`max_inflight > 1` *and* `interleaves > 0`. No single one of those four facts
proves the case alone.
-- **D** — `sequential` reports `max_inflight=1` and `interleaves=0`; `both`
+- **D**: `sequential` reports `max_inflight=1` and `interleaves=0`; `both`
reports many CPUs, real interleaving, and `max_inflight > cpus_allowed`.
-- **E** — C++26 Contracts (P2900) on GCC 16: one source, four binaries, one per
+- **E**: C++26 Contracts (P2900) on GCC 16: one source, four binaries, one per
`-fcontract-evaluation-semantic`, asserting four *distinct observable
- behaviors* (diagnostic present or absent, crossed with terminating or not) —
+ behaviors* (diagnostic present or absent, crossed with terminating or not);
never four exit codes.
-- **F** — safety hardening: the hardened build traps and names the failed
+- **F**: safety hardening: the hardened build traps and names the failed
predicate `__n < this->size()`. The unchecked build's runtime behavior is
undefined and is never asserted on; instead `nm` asserts **statically** that
it contains no `__glibcxx_assert_fail` at all.
-**G** (clang parity — a clang-built `conc` must produce the same digest) runs
+**G** (clang parity: a clang-built `conc` must produce the same digest) runs
if `clang++` is on `PATH` and prints an informational `SKIP` otherwise.
Not gated: `std::execution` senders (P2300) and static reflection (P2996).
-Measured on the reference host, neither exists — `__cpp_lib_senders` is
+Measured on the reference host, neither exists: `__cpp_lib_senders` is
undefined and GCC 16's `` is still the C++17 parallel-algorithms
header, while `__cpp_impl_reflection` is undefined in GCC 16 and clang 22
rejects `-freflection`. The chapter covers both as clearly-labeled
@@ -137,7 +137,7 @@ result is consumed through an atomic `sink`. Neither is decoration. Measured at
| loop-invariant | present | `max_inflight=1–2` | `max_inflight=1` |
| loop-invariant | absent | `max_inflight=1` | `max_inflight=1` |
-Drop either one and at least one compiler optimizes the workload away — workers
+Drop either one and at least one compiler optimizes the workload away: workers
finish in microseconds, never overlap, and `concurrent` reports
`max_inflight=1 interleaves=0`, the signature of a sequential run, whichever
model was asked for. An earlier version of this example measured the optimizer
diff --git a/examples/50-pthreads/README.md b/examples/50-pthreads/README.md
index 80da05d..0414eb6 100644
--- a/examples/50-pthreads/README.md
+++ b/examples/50-pthreads/README.md
@@ -1,6 +1,6 @@
-# 50 — pthreads
+# 50: pthreads
-Chapter 49 measured threads from above, with `std::thread`. This example goes
+The previous chapter measured threads from above, with `std::thread`. This example goes
**under** `std::thread` to the thing it is on Linux: a pthread. Each subcommand
exercises one per-thread control that POSIX offers and the C++ standard library
does not expose, and reports something the kernel or glibc produced.
@@ -23,15 +23,16 @@ pthreads report: facet=naming digest=0x481984990deee5ff
| `sched` | `pthread_getschedparam` / `setschedparam` | default is `SCHED_OTHER` at priority 0; an unprivileged `SCHED_FIFO` request is refused with `EPERM` |
| `cancel` | `pthread_cancel` | the C++ destructor **and** the `pthread_cleanup_push` handler both run; `pthread_join` reports `PTHREAD_CANCELED` |
| `cancel-swallow` | the same, mishandled | a `catch (...)` that does not rethrow aborts the process: `FATAL: exception not rethrown`, exit 134 |
-| `bridge` | `std::thread::native_handle()` | naming a `std::thread` from outside lands in that thread's procfs `comm` — `native_handle_type` **is** `pthread_t`, checked by `static_assert` |
+| `bridge` | `std::thread::native_handle()` | naming a `std::thread` from outside lands in that thread's procfs `comm`: `native_handle_type` **is** `pthread_t`, checked by `static_assert` |
One subcommand per facet is deliberate: `cancel-swallow` ends in `abort()` by
design, and a facet that aborts must never be able to take the others with it.
Every facet prints `digest=0x481984990deee5ff` over the payload
-`The quick brown.` — the same value ch46's C++, ch47's Go, ch48's Rust, and
-ch49's `conc` produce. As always, the answer does not depend on how the work
-was scheduled, or here on which per-thread knob was turned.
+`The quick brown.`: the same value the C++, Go, and Rust toolbox chapters,
+and the concurrency-vs-parallelism chapter's `conc`, produce. As always,
+the answer does not depend on how the work was scheduled, or here on which
+per-thread knob was turned.
## Two identities, and why it matters
@@ -44,7 +45,7 @@ A Linux thread has two names and they are not interchangeable:
a directory in `/proc`, the thing `perf`, `ftrace`, `top -H`, and gdb all
report.
-The main thread is the case that catches people out — its tid *equals* the
+The main thread is the case that catches people out: its tid *equals* the
process pid, because a process on Linux is its first thread.
## Layout
@@ -60,11 +61,12 @@ process pid, because a process on Linux is its first thread.
└── src/pthreads.cpp
```
-C++-only, like ch49. Go and Rust wrap these same pthreads and deliberately hide
-exactly these controls, so a three-language version would have two directories
-with nothing to say. `verify.lua` skips for any `LSP_LANG` other than `cpp`.
+C++-only, like the concurrency-vs-parallelism chapter. Go and Rust wrap these
+same pthreads and hide exactly these controls, so a three-language version
+would have two directories with nothing to say. `verify.lua` skips for any
+`LSP_LANG` other than `cpp`.
-Standard library and pthreads only. Local, unprivileged, offline — **no Boost,
+Standard library and pthreads only. Local, unprivileged, offline: **no Boost,
no Conan, no network, no VM, no root**.
## Build and run
@@ -98,7 +100,7 @@ terminal:
Two rows for one process: `TID` is the same integer `gettid()` returns, and the
first row's TID equals the PID because a process is its first thread. `PSR` is
-the CPU each thread is on — the `affinity` facet is what makes those two numbers
+the CPU each thread is on: the `affinity` facet is what makes those two numbers
something you chose rather than something the scheduler picked. `COMMAND` is the
procfs `comm` field, which defaults to the process name; the `naming` and
`bridge` facets are the ones that change it.
@@ -111,14 +113,14 @@ procfs `comm` field, which defaults to the process name; the `naming` and
Gates A–H are hard and all run offline and unprivileged:
-- **A** — every facet computes the expected digest.
-- **B** — identity: main tid == pid, worker tids distinct, `/proc/self/task/` present.
-- **C** — naming: both read-back paths agree, and 16 chars is refused with `ERANGE` leaving the old name in place.
-- **D** — stack: the custom thread got at least what was asked and less than the default; a sub-minimum request is refused with `EINVAL` rather than rounded up.
-- **E** — affinity: two threads on two different CPUs, caller's mask unchanged. Prints an informational `SKIP` (never a FAIL) on a host allowed fewer than 2 CPUs.
-- **F** — sched: `SCHED_OTHER` at priority 0, and `SCHED_FIFO` refused with `EPERM`. **The refusal is the gate** — asserting a *successful* `SCHED_FIFO` would need root and would not run on a reader's laptop.
-- **G** — cancel, both directions: the unwind runs destructors and cleanup handlers, and swallowing that unwind kills the process with `exception not rethrown`.
-- **H** — bridge: `native_handle_type` is `pthread_t`, and a `std::thread` named through it shows that name in procfs.
+- **A**: every facet computes the expected digest.
+- **B**: identity: main tid == pid, worker tids distinct, `/proc/self/task/` present.
+- **C**: naming: both read-back paths agree, and 16 chars is refused with `ERANGE` leaving the old name in place.
+- **D**: stack: the custom thread got at least what was asked and less than the default; a sub-minimum request is refused with `EINVAL` rather than rounded up.
+- **E**: affinity: two threads on two different CPUs, caller's mask unchanged. Prints an informational `SKIP` (never a FAIL) on a host allowed fewer than 2 CPUs.
+- **F**: sched: `SCHED_OTHER` at priority 0, and `SCHED_FIFO` refused with `EPERM`. **The refusal is the gate**: asserting a *successful* `SCHED_FIFO` would need root and would not run on a reader's laptop.
+- **G**: cancel, both directions: the unwind runs destructors and cleanup handlers, and swallowing that unwind kills the process with `exception not rethrown`.
+- **H**: bridge: `native_handle_type` is `pthread_t`, and a `std::thread` named through it shows that name in procfs.
**I** (clang parity on the digest) runs if `clang++` is on `PATH` and prints an
informational `SKIP` otherwise.
diff --git a/examples/51-std-threading/README.md b/examples/51-std-threading/README.md
index d9c7efd..649c458 100644
--- a/examples/51-std-threading/README.md
+++ b/examples/51-std-threading/README.md
@@ -1,20 +1,21 @@
-# 51 — the standard threading library, and what it costs
+# 51: the standard threading library, and what it costs
-Chapter 25 already showed that a mutex is a futex whose uncontended fast path
-stays in user space, measuring ~98% of acquisitions never reaching the kernel.
-This example starts there and pushes on three things ch25 did not cover:
+The futex chapter already showed that a mutex is a futex whose uncontended
+fast path stays in user space, measuring ~98% of acquisitions never reaching
+the kernel. This example starts there and pushes on three things that chapter
+did not cover:
-- **how far "nearly free" goes** — with the blocking stripped away and a control
+- **how far "nearly free" goes**: with the blocking stripped away and a control
to subtract, an uncontended `std::mutex` costs *zero* syscalls, not merely few
-- **the family** — `condition_variable`, `latch`, `barrier`,
+- **the family**: `condition_variable`, `latch`, `barrier`,
`counting_semaphore`, and `atomic::wait` are one mechanism, not five
-- **what the library adds over the futex** — `std::scoped_lock` will not
- deadlock where `lock_guard` will, and `std::stop_token` answers ch50's
- cancellation problem
+- **what the library adds over the futex**: `std::scoped_lock` will not
+ deadlock where `lock_guard` will, and `std::stop_token` answers the pthreads
+ chapter's cancellation problem
## The headline
-Three cases, all performing exactly the same 200,000 increments — only the
+Three cases, all performing exactly the same 200,000 increments; only the
contention differs:
```
@@ -27,7 +28,7 @@ uncontended futex=1
contended futex=2360
```
-`baseline` uses no mutex at all, and it still reports 1 — that call is the
+`baseline` uses no mutex at all, and it still reports 1: that call is the
dynamic loader, not a lock. `uncontended` performs 200,000 `lock_guard`
acquisitions and reports **the same 1**. Its entire syscall profile is 61
calls, all `execve`/`mmap`/`openat`/`read`/`close`:
@@ -40,13 +41,13 @@ calls, all `execve`/`mmap`/`openat`/`read`/`close`:
```
An uncontended `std::mutex` is an atomic compare-exchange in user space. The
-cost people attribute to "locking" is really the cost of **contention**.
+cost people attribute to "locking" is the cost of **contention**.
## The subcommands
| subcommand | what it measures | futex calls (3 runs) |
| --- | --- | --- |
-| `baseline` | 200,000 increments, no mutex — the control | 1, 1, 1 |
+| `baseline` | 200,000 increments, no mutex (the control) | 1, 1, 1 |
| `uncontended` | the same 200,000, each under a `lock_guard`, one thread | 1, 1, 1 |
| `contended` | the same 200,000, split across 8 threads | 2360, 2199, 2469 |
| `condvar` | `condition_variable::wait` with a predicate | 5, 5, 5 |
@@ -54,17 +55,17 @@ cost people attribute to "locking" is really the cost of **contention**.
| `barrier` | `std::barrier` (reusable, with a completion function) | 5, 4, 6 |
| `semaphore` | `std::counting_semaphore::acquire` | 4, 4, 4 |
| `atomic-wait` | `std::atomic::wait` / `notify_one` (C++20) | 4, 4, 4 |
-| `stoptoken` | `jthread` + `request_stop()` — the ch50 contrast | 2, 2, 2 |
-| `deadlock-safe` | `scoped_lock(a,b)` vs `scoped_lock(b,a)`, 100,000 acquisitions | — |
-| `deadlock-naive` | the same with `lock_guard` — **hangs on purpose** | — |
+| `stoptoken` | `jthread` + `request_stop()` (the pthreads-chapter contrast) | 2, 2, 2 |
+| `deadlock-safe` | `scoped_lock(a,b)` vs `scoped_lock(b,a)`, 100,000 acquisitions | n/a |
+| `deadlock-naive` | the same with `lock_guard` (**hangs by design**) | n/a |
Five different type names for the blocking primitives, one mechanism
underneath. A thread that must sleep until further notice has to tell the
kernel; there is no other way to do it.
-Every non-hanging subcommand prints `digest=0x481984990deee5ff` over the payload
-`The quick brown.` — the value ch46's C++, ch47's Go, ch48's Rust, ch49's
-`conc`, and ch50's `pthreads` all produce.
+Every non-hanging subcommand prints `digest=0x481984990deee5ff` over the
+payload `The quick brown.`: the same value every earlier toolbox and
+concurrency-model example in this book produces.
## Deadlock, both directions
@@ -78,7 +79,7 @@ exit=124
```
`std::scoped_lock` takes all its mutexes at once with a deadlock-avoidance
-algorithm — try, back off, retry — rather than locking them in argument order.
+algorithm (try, back off, retry) rather than locking them in argument order.
So `scoped_lock(a, b)` on one thread and `scoped_lock(b, a)` on the other is
safe. `lock_guard` in those same orders is the textbook deadlock.
@@ -100,8 +101,8 @@ Three threads, not two: both workers plus the main thread blocked in `join()`.
All of them parked in the same kernel function, readable unprivileged.
**`deadlock-naive` never returns.** It is its own subcommand for the same reason
-ch50's `cancel-swallow` was, and `verify.lua` only ever invokes it under
-`timeout(1)`.
+the pthreads chapter's `cancel-swallow` was, and `verify.lua` only ever
+invokes it under `timeout(1)`.
## Layout
@@ -116,8 +117,8 @@ ch50's `cancel-swallow` was, and `verify.lua` only ever invokes it under
└── src/stdthread.cpp
```
-C++-only, like ch49 and ch50. Standard library only — no Boost, no Conan, no
-network, no VM, no root.
+C++-only, like the concurrency-vs-parallelism and pthreads chapters. Standard
+library only: no Boost, no Conan, no network, no VM, no root.
## Build and run
@@ -143,18 +144,18 @@ any subcommand:
Gates A–F are hard:
-- **A** — every non-hanging subcommand computes the expected digest, and the
+- **A**: every non-hanging subcommand computes the expected digest, and the
three cost cases each report `acc=200000` so their syscall counts are
comparable in the first place.
-- **B** — the headline: `uncontended` costs no more futex calls than the
- no-mutex `baseline`, while `contended` — the identical work — costs hundreds,
+- **B**: the headline: `uncontended` costs no more futex calls than the
+ no-mutex `baseline`, while `contended` (the identical work) costs hundreds,
more than two orders of magnitude more.
-- **C** — `condvar`, `latch`, `barrier`, `semaphore`, and `atomic-wait` each
+- **C**: `condvar`, `latch`, `barrier`, `semaphore`, and `atomic-wait` each
issue at least one futex call.
-- **D** — `deadlock-safe` completes 100,000 acquisitions in opposing orders.
-- **E** — `deadlock-naive` times out (exit 124), and while hung every task's
+- **D**: `deadlock-safe` completes 100,000 acquisitions in opposing orders.
+- **E**: `deadlock-naive` times out (exit 124), and while hung every task's
`wchan` reads a futex wait.
-- **F** — `stoptoken`'s worker returns normally after `request_stop()`: no
+- **F**: `stoptoken`'s worker returns normally after `request_stop()`: no
`PTHREAD_CANCELED`, no forced unwind.
**G** (clang parity) runs if `clang++` is present, otherwise prints an
@@ -166,6 +167,6 @@ skipped; if `/proc//wchan` is unreadable, the wchan half of gate E is.
Neither ever becomes a false PASS.
**Not gated:** the absolute futex count for the contended case. It is
-scheduling-dependent — 1844–3111 observed across runs on the reference host —
-so only the *sign* of the comparison is asserted, per ch39. `verify.lua` prints
-a line saying so.
+scheduling-dependent (1844-3111 observed across runs on the reference host),
+so only the *sign* of the comparison is asserted, per the benchmarking
+chapter's rule. `verify.lua` prints a line saying so.
diff --git a/examples/52-boost-thread/README.md b/examples/52-boost-thread/README.md
index 3736cbf..05dcb3c 100644
--- a/examples/52-boost-thread/README.md
+++ b/examples/52-boost-thread/README.md
@@ -1,6 +1,6 @@
-# 52 — Boost.Thread: what the standard did not take
+# 52: Boost.Thread, what the standard did not take
-Boost.Thread is where `std::thread` came from — C++11 standardized it almost
+Boost.Thread is where `std::thread` came from: C++11 standardized it almost
wholesale. So the question worth asking in 2026 is not "how do I use
Boost.Thread" but **what survived standardization**, and the answer is specific,
still missing from C++23, and measurable.
@@ -12,32 +12,33 @@ versions: std::future_has_then=no boost::future_has_then=yes
```
That second line is a `concept` the compiler evaluated against both types on
-this run. The chapter does not claim `std::future` lacks continuations — it
+this run. The chapter does not claim `std::future` lacks continuations: it
compiles the question.
## The four pillars
| pillar | what Boost has | what `std::` has |
| --- | --- | --- |
-| `continuations` / `when-all` | `future::then()`, `when_all` — compose without blocking | `std::future` has neither |
+| `continuations` / `when-all` | `future::then()`, `when_all` (composes without blocking) | `std::future` has neither |
| `upgrade` | `upgrade_lock` → `upgrade_to_unique_lock`, a **third lock state** | `std::shared_mutex` has no upgrade path; `std::upgrade_lock` is undeclared |
-| `interrupt` / `interrupt-busy` | `thread::interrupt()` — an **ordinary C++ exception** at defined points | `stop_token` is a flag; nothing is thrown |
-| `attributes` | `thread::attributes::set_stack_size` — portable stack sizing | nothing at all (ch50's territory) |
+| `interrupt` / `interrupt-busy` | `thread::interrupt()` (an **ordinary C++ exception** at defined points) | `stop_token` is a flag; nothing is thrown |
+| `attributes` | `thread::attributes::set_stack_size` (portable stack sizing) | nothing at all (the pthreads chapter's territory) |
-Every pillar prints `digest=0x481984990deee5ff` over `The quick brown.` — the
-value ch46's C++, ch47's Go, ch48's Rust, ch49's `conc`, ch50's `pthreads`, and
-ch51's `stdthread` all produce.
+Every pillar prints `digest=0x481984990deee5ff` over `The quick brown.`, the
+same value produced by the C++, Go, and Rust toolbox chapters and by the
+concurrency-vs-parallelism, pthreads, and std::threading chapters (`conc`,
+`pthreads`, `stdthread`).
-## Cancellation, three ways — the payoff
+## Cancellation, three ways: the payoff
-This example completes an arc ch50 opened. **One mistake — `catch (...)` with no
-rethrow — against three cancellation models:**
+This example completes an arc the pthreads chapter opened. **One mistake
+(`catch (...)` with no rethrow) against three cancellation models:**
-| chapter | mechanism | swallow it and don't rethrow |
+| example | mechanism | swallow it and don't rethrow |
| --- | --- | --- |
-| ch50 | `pthread_cancel` — glibc **forced unwind** | **process aborts**, `FATAL: exception not rethrown`, exit 134 |
-| **ch52** | `thread::interrupt()` — an **ordinary exception** | **survives**, exit 0 |
-| ch51 | `stop_token` — a **flag** | nothing to swallow |
+| pthreads | `pthread_cancel`, glibc **forced unwind** | **process aborts**, `FATAL: exception not rethrown`, exit 134 |
+| **Boost.Thread (here)** | `thread::interrupt()`, an **ordinary exception** | **survives**, exit 0 |
+| std::thread | `stop_token`, a **flag** | nothing to swallow |
```
[host]$ ./demo.sh cpp run interrupt-swallow; echo "exit=$?"
@@ -46,7 +47,7 @@ interrupt-swallow: process survived -- boost interruption is an ordinary excepti
exit=0
```
-The difference is not how careful the caller is — it is what the mechanism *is*.
+What the mechanism *is* matters more than how careful the caller is.
A forced unwind must propagate; an ordinary exception need not.
Interruption points are a **defined list** (`this_thread::sleep_for`,
@@ -59,11 +60,12 @@ interrupt-busy: interrupt() delivered, worker_stopped=no
interrupt-busy: a loop with no interruption point never checks, so it never stops
```
-That is the trade, and it is gated. ch50's `pthread_cancel` *can* yank a thread
-out of a compute loop because it is asynchronous; Boost gives that up in
-exchange for interruption happening only where you can see it happening.
+That is the trade, and it is gated. The pthreads chapter's `pthread_cancel`
+*can* yank a thread out of a compute loop because it is asynchronous; Boost
+gives that up in exchange for interruption happening only where you can see
+it happening.
-## The gotcha that will bite you
+## A constraint that will bite you
`boost::thread::attributes` **must be passed as a `const` lvalue**:
@@ -93,13 +95,13 @@ adding a dummy argument does not help.
└── src/boostthread.cpp
```
-**System Boost only — no Conan, no network.** Fedora's `boost-devel` (1.90.0
+**System Boost only: no Conan, no network.** Fedora's `boost-devel` (1.90.0
here) provides everything. Local, unprivileged, no VM, no root.
The `BOOST_THREAD_VERSION` and continuation macros live in `CMakeLists.txt`
-rather than the `.cpp`, deliberately: Boost reads them while its own headers are
+rather than the `.cpp`: Boost reads them while its own headers are
being preprocessed, so a define landing after the first Boost include is
-silently ignored — and the symptom is a missing `.then()` rather than a warning.
+silently ignored, and the symptom is a missing `.then()` rather than a warning.
## Build and run
@@ -123,22 +125,23 @@ Bare `./demo.sh` builds and runs `versions`.
Gates A–F are hard:
-- **A** — every pillar computes the expected digest.
-- **B** — `.then()` chains `async(21)` into 42; `when_all` joins two futures.
-- **C** — the `std::future` contrast is **compile-time**: a concept evaluated
+- **A**: every pillar computes the expected digest.
+- **B**: `.then()` chains `async(21)` into 42; `when_all` joins two futures.
+- **C**: the `std::future` contrast is **compile-time**: a concept evaluated
against both types, so the claim is checked by the compiler on every run.
-- **D** — a concurrent reader takes a shared lock while the upgrade lock is
+- **D**: a concurrent reader takes a shared lock while the upgrade lock is
held, then the upgrade promotes in place and writes.
-- **E** — interruption is caught and the worker joins normally; the swallow
- case exits 0 where ch50's equivalent exited 134; and a busy loop with no
- interruption point ignores `interrupt()` entirely.
-- **F** — a 256 KiB Boost attribute yields exactly **262144** via
- `pthread_getattr_np` — byte-identical to ch50's POSIX measurement, because on
- Linux it is that call.
+- **E**: interruption is caught and the worker joins normally; the swallow
+ case exits 0 where the pthreads chapter's equivalent exited 134; and a busy
+ loop with no interruption point ignores `interrupt()` entirely.
+- **F**: a 256 KiB Boost attribute yields exactly **262144** via
+ `pthread_getattr_np`, byte-identical to the pthreads chapter's POSIX
+ measurement, because on Linux it is that call.
**G** (clang parity) runs if `clang++` is present, otherwise prints an
informational `SKIP`.
**Not gated:** Boost's experimental executors. This example covers what the
standard did *not* take, and executors are the part C++26's `std::execution` is
-actively replacing — forward-looking in ch49, compared for real in ch56.
+actively replacing: forward-looking in the concurrency-vs-parallelism chapter,
+compared directly in the final comparison chapter.
diff --git a/examples/53-coroutines/README.md b/examples/53-coroutines/README.md
index 4635416..df236c6 100644
--- a/examples/53-coroutines/README.md
+++ b/examples/53-coroutines/README.md
@@ -1,13 +1,14 @@
-# 53 — C++20 coroutines: what a suspended computation costs
+# 53: C++20 coroutines, what a suspended computation costs
-Chapter 27 already built a working coroutine engine — `promise_type`, the
-awaitable protocol, `await_suspend` returning a handle for symmetric transfer,
-and an epoll reactor that parks and resumes handles. **That chapter owns the
-mechanism, and this example does not repeat any of it.**
+The async-runtimes chapter already built a working coroutine engine:
+`promise_type`, the awaitable protocol, `await_suspend` returning a handle for
+symmetric transfer, and an epoll reactor that parks and resumes handles.
+**That chapter owns the mechanism, and this example does not repeat any of
+it.**
-This one asks the question ch27 never did, and it is a systems question: a
-coroutine frame and a thread both hold one paused computation, so what does each
-one cost?
+This one asks a question the async-runtimes chapter never did, and it is a
+systems question: a coroutine frame and a thread both hold one paused
+computation, so what does each one cost?
```
[host]$ ./demo.sh cpp run versus-thread
@@ -15,23 +16,23 @@ versus-thread: coroutine_frame=32 bytes thread_stack=8388608 bytes (ch50)
versus-thread: ratio=262144x frames_per_thread_stack=262144
```
-Chapter 50 measured that 8 MiB with `pthread_getattr_np`. The 32 bytes is
-measured here. **262,144 coroutine frames fit in one thread's stack** — which is
-the entire reason coroutines exist.
+The pthreads chapter measured that 8 MiB with `pthread_getattr_np`. The 32
+bytes is measured here. **262,144 coroutine frames fit in one thread's
+stack**, which is the entire reason coroutines exist.
## The cases
| subcommand | what it measures |
| --- | --- |
| `frames` | frame size for three shapes: 32 / 304 / 4128 bytes |
-| `halo` | whether the compiler elides the frame allocation — **it depends** |
-| `generator` | C++23 `std::generator`, which ch27 had to hand-roll |
-| `versus-thread` | frame bytes against ch50's measured thread stack |
+| `halo` | whether the compiler elides the frame allocation: **it depends** |
+| `generator` | C++23 `std::generator`, which the async-runtimes chapter had to hand-roll |
+| `versus-thread` | frame bytes against the pthreads chapter's measured thread stack |
| `lifetime` | the trap: a `coroutine_handle` owns nothing |
| `versions` | what the toolchain reports |
-Every case prints `digest=0x481984990deee5ff` — the value ch46 through ch52 all
-produce.
+Every case prints `digest=0x481984990deee5ff`: the same value every toolbox
+and concurrency-model example in this book produces.
## How the frame is measured
@@ -55,7 +56,7 @@ frames: trivial=32 small=304 large=4128
```
The frame holds the promise, resume/destroy pointers, a state index, and **the
-locals that are live across a suspension** — nothing else. A coroutine carrying
+locals that are live across a suspension**, nothing else. A coroutine carrying
a `char[4096]` across a `co_await` has a 4128-byte frame; one carrying nothing
has 32. Cost is what you carry, not a flat overhead.
@@ -63,7 +64,7 @@ has 32. Cost is what you carry, not a flat overhead.
`[dcl.fct.def.coroutine]` says an implementation **may** elide the frame
allocation when the frame provably does not outlive its caller. May. There is no
-conforming way to depend on it — and on this host the two compilers disagree
+conforming way to depend on it, and on this host the two compilers disagree
completely.
1000 calls to an eager coroutine that never suspends, the easiest possible case:
@@ -71,12 +72,12 @@ completely.
| compiler | -O0 | -O1 | -O2 | -O3 |
| --- | --- | --- | --- | --- |
| **g++ 16.1.1** | 1000 | 1000 | 1000 | 1000 |
-| **clang++ 22.1.8** | 1000 | **0** | **0** | — |
+| **clang++ 22.1.8** | 1000 | **0** | **0** | n/a |
Same source, same standard, same `-O2`: a thousand heap allocations, or none.
"The optimizer will remove it" is a claim about your compiler, not about C++.
-`verify.lua` deliberately **does not gate on whether elision happens** — that
+`verify.lua` **does not gate on whether elision happens**: that
would encode one compiler's choice as a rule. It gates that the count is
measured and internally consistent, and gate F compares the two compilers.
@@ -93,7 +94,7 @@ the frame, it is not RAII, copying it copies a pointer, and destroying it frees
nothing. Exactly one `destroy()` per frame, and the type system will not remind
you.
-Note the observable is the **free** count, not the allocation count — both
+Note the observable is the **free** count, not the allocation count: both
halves allocate one frame, so allocations cannot tell them apart. The question
is whether anyone called `destroy()`.
@@ -110,7 +111,7 @@ is whether anyone called `destroy()`.
└── src/coro.cpp
```
-C++23 standard library only — **no threads, no Boost, no network**. Local and
+C++23 standard library only: **no threads, no Boost, no network**. Local and
unprivileged. C++23 rather than C++20 for ``.
## Build and run
@@ -136,17 +137,17 @@ To reproduce the HALO matrix yourself:
[host]$ LSP_LANG=cpp lua verify.lua
```
-- **A** — every case computes the expected digest.
-- **B** — frames track live-across-suspend locals: trivial is overhead-only, the
+- **A**: every case computes the expected digest.
+- **B**: frames track live-across-suspend locals: trivial is overhead-only, the
`char[4096]` case exceeds 4096, and the middle sits between. **Relationships
- are gated, never exact byte counts** — those are ABI-dependent.
-- **C** — HALO is measured and internally consistent. Deliberately *not* an
+ are gated, never exact byte counts**: those are ABI-dependent.
+- **C**: HALO is measured and internally consistent. This is *not* an
assertion that elision does or does not happen, so a future GCC that starts
eliding will not fail the gate.
-- **D** — `std::generator` produces the correct 20th Fibonacci term (4181) and
+- **D**: `std::generator` produces the correct 20th Fibonacci term (4181) and
is destroyed mid-suspension.
-- **E** — the frame is at least three orders of magnitude below ch50's 8388608,
- and the ratio is the arithmetic of the two measured numbers rather than a
- quoted figure.
+- **E**: the frame is at least three orders of magnitude below the pthreads
+ chapter's 8388608, and the ratio is the arithmetic of the two measured
+ numbers rather than a quoted figure.
**F** (clang parity + the HALO contrast) runs if `clang++` is present.
diff --git a/examples/54-boost-fiber/README.md b/examples/54-boost-fiber/README.md
index fb3e55d..42aa4af 100644
--- a/examples/54-boost-fiber/README.md
+++ b/examples/54-boost-fiber/README.md
@@ -1,8 +1,8 @@
-# 54 — Boost.Fiber: what a real stack buys
+# 54: Boost.Fiber, what a real stack buys
-Chapter 53 measured a C++20 coroutine frame at **32 bytes** — stackless, holding
-only the locals that cross a suspension. A Boost fiber is **stackful**: it owns
-a real machine stack, switched by `boost::context`.
+The previous chapter measured a C++20 coroutine frame at **32 bytes**:
+stackless, holding only the locals that cross a suspension. A Boost fiber is
+**stackful**: it owns a real machine stack, switched by `boost::context`.
That one difference produces both its price and its capability, and this example
measures both.
@@ -18,9 +18,9 @@ versus: fiber_between_the_two=yes
| paused computation | cost | measured in |
| --- | --- | --- |
-| thread | 8388608 B (8 MiB) | ch50, `pthread_getattr_np` |
-| **fiber** | **131072 B (128 KiB)** | ch54, `stack_traits::default_size()` |
-| coroutine frame | 32 B | ch53, `operator new` in `promise_type` |
+| thread | 8388608 B (8 MiB) | pthreads chapter, `pthread_getattr_np` |
+| **fiber** | **131072 B (128 KiB)** | this chapter, `stack_traits::default_size()` |
+| coroutine frame | 32 B | coroutines chapter, `operator new` in `promise_type` |
**8 MiB → 128 KiB → 32 bytes**, roughly two orders of magnitude at each step,
every number measured on this host rather than quoted. Both ratios are computed
@@ -36,13 +36,13 @@ deep: yielded_at_depth=6 resumed_at_depth=6 same=yes
deep: suspended from a plain function 6 frames below the fiber body -- no co_await, no coroutine, no colouring
```
-`leaf()` is an ordinary function. So is `level()`. Neither is a coroutine — yet
+`leaf()` is an ordinary function. So is `level()`. Neither is a coroutine, yet
the fiber suspends from inside `leaf()`, six frames below where the fiber body
started, and resumes exactly there.
**A stackless coroutine cannot do this at any price.** `co_await` is only valid
in the body of a coroutine, so every frame between the suspension point and the
-scheduler must itself be a coroutine — the "function colouring" problem. 128 KiB
+scheduler must itself be a coroutine: the "function colouring" problem. 128 KiB
of stack is exactly what buys the ability to suspend anywhere.
## The scheduler is a choice
@@ -58,14 +58,14 @@ sharedwork: fibers=16 worker_threads=4 distinct_tids=4
```
The default `round_robin` scheduler keeps a per-thread queue and never migrates
-a fiber, so 16 tasks are in flight on one CPU — **concurrent, not parallel**, in
-ch49's exact terms. One call to
+a fiber, so 16 tasks are in flight on one CPU: **concurrent, not parallel**, in
+the concurrency-vs-parallelism chapter's exact terms. One call to
`use_scheduling_algorithm()` puts them on a shared queue that
four threads pull from, and the same fibers now occupy four CPUs.
-Chapter 44 covered Go's answer: the GMP runtime decides, automatically, and
-mostly will not be argued with. C++ makes you choose — more work, more control,
-and the choice is observable in `gettid()`.
+The Go runtime chapter covered Go's answer: the GMP runtime decides,
+automatically, and mostly will not be argued with. C++ makes you choose:
+more work, more control, and the choice is observable in `gettid()`.
## The guard page
@@ -77,7 +77,7 @@ stacks: exact_request=yes guard_is_one_page=yes
```
`fixedsize_stack` gives exactly what you ask for. `protected_fixedsize_stack`
-gives exactly one page more — and that page is mapped `PROT_NONE`, so running
+gives exactly one page more, and that page is mapped `PROT_NONE`, so running
off the end of the stack faults instead of corrupting whatever is mapped next:
```
@@ -90,10 +90,11 @@ guard-overflow: recursing 1000 frames on a 65536-byte protected stack; this is e
# exit 139 = SIGSEGV
```
-`guard-overflow` dies by design and is its own subcommand for the reason ch50's
-`cancel-swallow` and ch51's `deadlock-naive` were. The gate asserts the **exit
-signal** and never anything the faulting run printed — output after a stack
-overflow is undefined, and gating on it would be gating on UB.
+`guard-overflow` dies by design and is its own subcommand for the reason the
+pthreads chapter's `cancel-swallow` and the std::threading chapter's
+`deadlock-naive` were. The gate asserts the **exit signal** and never
+anything the faulting run printed: output after a stack overflow is
+undefined, and gating on it would be gating on UB.
## Layout
@@ -108,14 +109,15 @@ overflow is undefined, and gating on it would be gating on UB.
└── src/fiberdemo.cpp
```
-**System Boost only — no Conan, no network.** Links `-lboost_fiber
+**System Boost only: no Conan, no network.** Links `-lboost_fiber
-lboost_context -pthread`. Local, unprivileged, no VM, no root.
-The translation unit is compiled `-O0` deliberately. At `-O2` both compilers may
+The translation unit is compiled `-O0` by design. At `-O2` both compilers may
inline the recursion into a loop or drop the `volatile` array's storage, and the
-fiber would never approach its guard page — `guard-overflow` would silently stop
-demonstrating anything. Chapter 49 learned that lesson when an unconsumed burn
-loop was deleted and the example measured the optimizer instead of the scheduler.
+fiber would never approach its guard page: `guard-overflow` would silently stop
+demonstrating anything. The concurrency-vs-parallelism chapter learned that
+lesson when an unconsumed burn loop was deleted and the example measured the
+optimizer instead of the scheduler.
## Build and run
@@ -134,22 +136,23 @@ loop was deleted and the example measured the optimizer instead of the scheduler
[host]$ LSP_LANG=cpp lua verify.lua
```
-- **A** — every non-faulting case computes the expected digest.
-- **B** — a `fixedsize_stack` returns exactly what was asked; the protected one
+- **A**: every non-faulting case computes the expected digest.
+- **B**: a `fixedsize_stack` returns exactly what was asked; the protected one
returns exactly one page more. **The guard delta is gated against
`page_size()`**, never a hardcoded 4096.
-- **C** — the fiber stack sits strictly between ch53's 32 B and ch50's
- 8388608 B, and both ratios are arithmetic on measured numbers.
-- **D** — a fiber suspends from an ordinary function more than one frame deep
+- **C**: the fiber stack sits strictly between the coroutine frame's 32 B and
+ the thread stack's 8388608 B, and both ratios are arithmetic on measured
+ numbers.
+- **D**: a fiber suspends from an ordinary function more than one frame deep
and resumes at the same depth.
-- **E** — `round_robin` reports exactly 1 tid; `shared_work` reports more than 1.
- Sign gated, never magnitude — with a bounded retry, since a loaded host could
+- **E**: `round_robin` reports exactly 1 tid; `shared_work` reports more than 1.
+ Sign gated, never magnitude, with a bounded retry, since a loaded host could
legitimately keep the work on fewer threads. Measured 4 of 4 on five runs here.
-- **F** — the in-budget fiber exits 0; the overflow dies of a signal. A `124`
- (timeout, i.e. hung rather than faulted) deliberately does **not** pass.
+- **F**: the in-budget fiber exits 0; the overflow dies of a signal. A `124`
+ (timeout, i.e. hung rather than faulted) does **not** pass, by design.
**G** (clang parity) runs if `clang++` is present.
**Not gated:** context-switch timings. This book does not gate on durations
-(ch39); the fiber-versus-thread claim here is about memory and about *where* a
-suspension may occur, both of which are counts.
+(see the benchmarking chapter); the fiber-versus-thread claim here is about
+memory and about *where* a suspension may occur, both of which are counts.
diff --git a/examples/55-boost-asio/README.md b/examples/55-boost-asio/README.md
index bb6a446..0ce9e59 100644
--- a/examples/55-boost-asio/README.md
+++ b/examples/55-boost-asio/README.md
@@ -1,13 +1,14 @@
-# 55 — Boost.Asio: what a library adds on top of a reactor
+# 55: Boost.Asio: what a library adds on top of a reactor
-Chapter 9 built an epoll loop by hand. Chapter 22 wrote the nonblocking/`EAGAIN`
-state machine. Chapter 27 drove a hand-rolled coroutine reactor over epoll.
-Those three own the readiness model, and **nothing here repeats it**.
+The event-loop chapter built an epoll loop by hand. The connection-scaling
+chapter wrote the nonblocking/`EAGAIN` state machine. The coroutines chapter
+drove a hand-rolled coroutine reactor over epoll. Those three own the
+readiness model, and **nothing here repeats it**.
This example measures the concepts Asio has that a hand-written loop does not:
who runs a handler, whether two can run at once, and what that guarantee costs.
-## `run()` returns when there is no work — not when you are done
+## `run()` returns when there is no work, not when you are done
```
[host]$ ./demo.sh cpp run work-guard
@@ -19,9 +20,9 @@ A freshly constructed `io_context` has no outstanding work, so `run()` returns
after **zero** handlers and every async operation you were about to start never
happens. `executor_work_guard` is the claim that says "more is coming".
-## The strand — the finding
+## The strand: the finding
-2000 posts, 8 threads on one `io_context`, a deliberately non-atomic `long`
+2000 posts, 8 threads on one `io_context`, a non-atomic `long`
counter incremented in every handler. The handler body is *the same function* in
both runs; only the executor it was posted to differs.
@@ -35,7 +36,7 @@ nostrand: posts=2000 counter=1862 expected=2000 lost=138
nostrand: max_inflight=6 overlaps=1492 serialized=no
```
-Serialization without a mutex, and — on the other arm — a real data race visible
+Serialization without a mutex, and (on the other arm) a real data race visible
as a **wrong number** rather than a sanitizer warning.
**The lost count is never gated.** An unsynchronized increment is a data race and
@@ -45,7 +46,7 @@ measured with atomics and is well defined.
## A strand is not a mutex, and it is measurably cheaper
-The same 20000 increments, counted with Chapter 51's method
+The same 20000 increments, counted with the std::thread chapter's method
(`strace -f -c -e trace=futex`):
| mechanism | futex calls | result |
@@ -54,8 +55,9 @@ The same 20000 increments, counted with Chapter 51's method
| `std::mutex` | **1022** | 20000 (correct) |
Both are correct. The difference is structural: a mutex **blocks a thread** on
-contention, which is what Chapter 51 measured as `FUTEX_WAIT`; a strand **defers
-the handler** and returns the thread to the pool to pick up other work.
+contention, which is what the std::thread chapter measured as `FUTEX_WAIT`; a
+strand **defers the handler** and returns the thread to the pool to pick up
+other work.
## Scatter-gather is one syscall
@@ -74,15 +76,15 @@ reached by handing Asio a buffer sequence instead of assembling an iovec array.
## Who runs the handlers
-400 posts, tids from `gettid()` (Chapter 50's instrument, Chapter 49's
-vocabulary):
+400 posts, tids from `gettid()` (the pthreads chapter's instrument, the
+concurrency-vs-parallelism chapter's vocabulary):
| topology | distinct tids |
| --- | --- |
-| `topology-one` — one thread, one context | **1** |
-| `topology-pool` — 8 threads, one context | **8** |
-| `topology-percore` — one context per thread | **4** |
-| `topology-threadpool` — `asio::thread_pool(4)` | **4** |
+| `topology-one`: one thread, one context | **1** |
+| `topology-pool`: 8 threads, one context | **8** |
+| `topology-percore`: one context per thread | **4** |
+| `topology-threadpool`: `asio::thread_pool(4)` | **4** |
Same handlers every time. The only variable is the shape of the `run()` call.
@@ -93,10 +95,11 @@ Same handlers every time. The only variable is the shape of the `run()` call.
cancel: handlers_run=1 handler_ran=yes error=Operation canceled aborted=yes
```
-Chapter 51 measured `std::stop_token` (a flag the target polls). Chapter 52
-measured `boost::thread::interrupt()` (an ordinary exception). Asio's is a
-**completion carrying `operation_aborted`** — a one-hour timer's handler runs
-immediately, exactly once, so the ownership rules never change.
+The std::thread chapter measured `std::stop_token` (a flag the target polls).
+The Boost.Thread chapter measured `boost::thread::interrupt()` (an ordinary
+exception). Asio's is a **completion carrying `operation_aborted`**: a
+one-hour timer's handler runs immediately, exactly once, so the ownership
+rules never change.
## Layout
@@ -111,7 +114,7 @@ immediately, exactly once, so the ownership rules never change.
└── src/asiodemo.cpp
```
-**System Boost only — no Conan, no network.** Asio is header-only here; the link
+**System Boost only: no Conan, no network.** Asio is header-only here; the link
line is `Boost::system` (for the error categories) plus `Threads::Threads`. The
translation unit is compiled `-DBOOST_ASIO_NO_DEPRECATED` so nothing accidentally
demonstrates an interface Asio has replaced.
@@ -136,25 +139,26 @@ unprivileged.
[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua
```
-- **A** — every one of the 13 cases computes `digest=0x481984990deee5ff`,
- byte-identical to Chapters 46 through 54.
-- **B** — a fresh `io_context::run()` returns after zero handlers; with a work
+- **A**: every one of the 13 cases computes `digest=0x481984990deee5ff`,
+ byte-identical across the toolbox-through-concurrency chapters.
+- **B**: a fresh `io_context::run()` returns after zero handlers; with a work
guard held it runs the posted one.
-- **C** — through a strand the unsynchronized counter is exactly 2000 with
+- **C**: through a strand the unsynchronized counter is exactly 2000 with
`max_inflight=1` and `overlaps=0`; posted straight to the context the same
- handler overlaps. **The overlap is gated, the lost count is not** — never gate
- on the output of undefined behavior (Chapter 49's rule).
-- **D** — both arms produce the correct 20000, and the strand issues materially
+ handler overlaps. **The overlap is gated, the lost count is not**: never gate
+ on the output of undefined behavior (the concurrency-vs-parallelism
+ chapter's rule).
+- **D**: both arms produce the correct 20000, and the strand issues materially
fewer futex calls. Gated as a **ratio** (`strand < mutex/2`), never as
magnitudes. Degrades to an informational SKIP if `strace` cannot attach.
-- **E** — the 4-buffer sequence costs exactly **1** socket write-family syscall
+- **E**: the 4-buffer sequence costs exactly **1** socket write-family syscall
and the separate form exactly **4**. These are deterministic, so they are gated
- exactly. Plain `write(2)` is excluded — that is this program's own stdout.
-- **F** — `topology-one` reports exactly 1 tid; the three multi-threaded shapes
+ exactly. Plain `write(2)` is excluded: that is this program's own stdout.
+- **F**: `topology-one` reports exactly 1 tid; the three multi-threaded shapes
report more than 1. Sign gated, never magnitude.
**G** (clang parity on the digest and on `overlaps=0`) runs if `clang++` is
present.
-**Not gated:** timings (Chapter 39 — this book does not gate on durations); the
-exact lost-increment count; anything needing a network port.
+**Not gated:** timings (the benchmarking chapter: this book does not gate on
+durations); the exact lost-increment count; anything needing a network port.
diff --git a/examples/56-capstone-comparison/README.md b/examples/56-capstone-comparison/README.md
index 255fd2f..b20d5f7 100644
--- a/examples/56-capstone-comparison/README.md
+++ b/examples/56-capstone-comparison/README.md
@@ -1,7 +1,7 @@
-# 56 — the capstone: one workload, seven models, two instruments
+# 56: the final project, one workload, seven models, two instruments
-Chapters 50 through 55 each measured one concurrency model. Each used its own
-instrument, on its own workload, to answer its own question — so **no two
+The six previous chapters each measured one concurrency model. Each used its
+own instrument, on its own workload, to answer its own question, so **no two
numbers in the compendium were strictly comparable.**
That is what this example fixes. One workload runs through every model, and
@@ -12,7 +12,7 @@ every model is measured the same two ways.
`cpp/src/workload.hpp` is shared verbatim by all seven arms: **8 tasks × 5000
rounds = 40000 folds** into one accumulator. The fold is addition on `uint64_t`,
which is commutative and associative, so the total does not depend on the order a
-model happens to run in — and the models genuinely differ in that order.
+model happens to run in, and the models do differ in that order.
```
[host]$ ./demo.sh cpp run sequential
@@ -24,23 +24,24 @@ sequential: distinct_tids=1
any comparison is drawn: a model that produced a different total would not be a
differently-priced answer to the same question, it would be a wrong one.
-## Instrument 1 — who ran it (`gettid()`, ch50's instrument)
+## Instrument 1: who ran it (`gettid()`, the pthreads chapter's instrument)
| arm | model | `distinct_tids` |
| --- | --- | --- |
| `sequential` | the control | **1** |
-| `coroutine` | C++20 coroutines (ch53) | **1** |
-| `fiber` | Boost.Fiber (ch54) | **1** |
-| `pthreads` | pthreads (ch50) | **8** |
-| `std-thread` | `std::jthread` (ch51) | **8** |
-| `boost-thread` | Boost.Thread (ch52) | **8** |
-| `asio` | Boost.Asio strand (ch55) | **8** |
+| `coroutine` | C++20 coroutines | **1** |
+| `fiber` | Boost.Fiber | **1** |
+| `pthreads` | pthreads | **8** |
+| `std-thread` | `std::jthread` | **8** |
+| `boost-thread` | Boost.Thread | **8** |
+| `asio` | Boost.Asio strand | **8** |
| `senders` | P2300 via stdexec (opt-in) | **8** |
-The first three hold **eight tasks in flight on one CPU** — Chapter 49's
-"concurrent, not parallel", as a count rather than an assertion.
+The first three hold **eight tasks in flight on one CPU**, the
+concurrency-vs-parallelism chapter's "concurrent, not parallel," as a count
+rather than an assertion.
-## Instrument 2 — what synchronization cost (`strace -f -c -e trace=futex`)
+## Instrument 2: what synchronization cost (`strace -f -c -e trace=futex`)
Three tiers, stable across runs:
@@ -55,7 +56,7 @@ then has to park and wake; a strand **defers a handler** and hands the thread
back. And a single-threaded model reaches the futex not at all, whatever its
suspension machinery costs elsewhere.
-**Magnitudes are never gated** — they move on every run. The three tiers are.
+**Magnitudes are never gated**: they move on every run. The three tiers are.
## The two assembled axes
@@ -74,11 +75,11 @@ table: ch52 thread::interrupt=exception (ordinary, at defined points)
table: ch55 cancellation_signal=completion (operation_aborted, handler still runs)
```
-## The seventh model — P2300 senders, and what it is not
+## The seventh model: P2300 senders, and what it is not
-Chapter 49 measured that the standard library has no P2300 and said the live
-comparison went into Chapter 56 *if and only if the toolchain had caught up*. It
-has not:
+The concurrency-vs-parallelism chapter measured that the standard library has
+no P2300 and said the live comparison went into this chapter *if and only if
+the toolchain had caught up*. It has not:
```
[host]$ ./demo.sh cpp run versions
@@ -97,7 +98,7 @@ senders: implementation=NVIDIA-stdexec (reference), stdlib_p2300=no
```
stdexec is **not in ConanCenter** and not packaged by Fedora, and upstream
-publishes no releases — only `nvhpc-*` snapshot tags. So
+publishes no releases, only `nvhpc-*` snapshot tags. So
`cpp/conan/recipe/conanfile.py` pins tag **`nvhpc-26.05`** by tarball **sha256**,
which is what makes the build reproducible; a tag alone can be moved.
@@ -122,9 +123,9 @@ which is what makes the build reproducible; a tag alone can be moved.
└── senders.cpp # the seventh, built only by the conan preset
```
-**The six-model core needs no Conan, no network, and no third-party package** —
-system Boost only. That isolation is deliberate and is what keeps `validate.yml`
-green on fedora:44, which has no Conan.
+**The six-model core needs no Conan, no network, and no third-party package**:
+system Boost only. That isolation keeps `validate.yml` green on fedora:44,
+which has no Conan.
## Build and run
@@ -144,9 +145,9 @@ The opt-in seventh arm, which needs Conan and (on a cold cache) the network:
[host]$ ./cpp/build/conan/capstone-senders senders
```
-> **The `conan` preset builds `Release`, not `RelWithDebInfo`, on purpose.**
+> **The `conan` preset builds `Release`, not `RelWithDebInfo`, by design.**
> CMakeDeps gates the include directories behind `$<$:...>`, so a
-> mismatched build type silently drops them — `find_package(stdexec)` still
+> mismatched build type silently drops them: `find_package(stdexec)` still
> *succeeds*, and the compile then fails on a missing header.
## Verification
@@ -155,19 +156,21 @@ The opt-in seventh arm, which needs Conan and (on a cold cache) the network:
[host]$ LSP_LANG=cpp REPO_ROOT=$(cd ../.. && pwd) lua verify.lua
```
-- **A** — all nine cases compute `digest=0x481984990deee5ff` (ch46–ch55's).
-- **B** — the capstone gate: every model folds to the identical total.
-- **C** — instrument 1: exactly 1 tid for the three single-threaded models,
+- **A**: all nine cases compute `digest=0x481984990deee5ff` (shared across
+ every toolbox and concurrency-model example in this book).
+- **B**: the final-project gate: every model folds to the identical total.
+- **C**: instrument 1: exactly 1 tid for the three single-threaded models,
more than 1 for the four multi-threaded ones. Signs, never magnitudes.
-- **D** — instrument 2: single-threaded models under 10 futex calls, lock-based
+- **D**: instrument 2: single-threaded models under 10 futex calls, lock-based
models over 100, and Asio's strand under **half** what `std::mutex` cost.
Degrades to an informational SKIP if `strace` cannot attach.
-- **F** — clang parity on the digest and on the total.
+- **F**: clang parity on the digest and on the total.
**E** (the senders arm) runs only if `conan` is on `PATH` and can supply stdexec;
otherwise it prints a SKIP and everything else still passes. Both paths were
exercised: **PASS 57 / FAIL 0** with Conan available, and **PASS 52 / FAIL 0**
with `conan` hidden from `PATH` and the Conan build tree deleted.
-**Not gated:** timings (ch39), futex magnitudes, and the ranking of models by
-futex count beyond the three-tier split.
+**Not gated:** timings (per the benchmarking chapter's rule), futex
+magnitudes, and the ranking of models by futex count beyond the three-tier
+split.
diff --git a/examples/_infra/README.md b/examples/_infra/README.md
index f77880a..1fb10ed 100644
--- a/examples/_infra/README.md
+++ b/examples/_infra/README.md
@@ -46,7 +46,7 @@ the stack is not up rather than failing.
Examples deployed to the lab VM (`TARGET=...` with `deploy-to-vm.sh`) cannot
reach `localhost` on the host. From a libvirt guest on the default NAT network,
-the host is reachable at the network's gateway IP — on this lab that is
+the host is reachable at the network's gateway IP: on this lab that is
`192.168.124.1` (libvirt's stock default is `192.168.122.1`; check yours with
`ip route show default` inside the guest). Pass the endpoint through
`OTEL_ENDPOINT`:
diff --git a/examples/_template/README.md b/examples/_template/README.md
index 39b523c..d90dd6a 100644
--- a/examples/_template/README.md
+++ b/examples/_template/README.md
@@ -1,7 +1,7 @@
-# _template — hello-syscall
+# _template: hello-syscall
The seed for every runnable example in the book. It implements the same tiny
-program three times — C++23, Go, and Rust — each printing exactly one line:
+program three times, C++23, Go, and Rust, each printing exactly one line:
```
pid on ()
@@ -26,9 +26,9 @@ _template/
Every language directory has a `demo.sh` with the same interface, identical
across the whole book:
-- `./demo.sh` — build then run
-- `./demo.sh build` — build only
-- `./demo.sh run [args]` — run the built binary
+- `./demo.sh`: build then run
+- `./demo.sh build`: build only
+- `./demo.sh run [args]`: run the built binary
- With env `TARGET` set, `run` deploys the binary to that lab VM via
`scripts/lab/deploy-to-vm.sh "$TARGET" -- [args]` instead of
running locally.
diff --git a/index.html b/index.html
index 1a35e4f..4863501 100644
--- a/index.html
+++ b/index.html
@@ -23,10 +23,10 @@ {{ site.title }}
-
3×Every example in modern C++23, Go, and Rust — switch languages with one click
-
VMPrivileged and two-host demos run in a disposable Fedora 44 KVM lab — never on your laptop
+
3×Every example in modern C++23, Go, and Rust: switch languages with one click
+
VMPrivileged and two-host demos run in a disposable Fedora 44 KVM lab, never on your laptop
📊Observe everything: bcc/bpftrace, perf, PCP, and OpenTelemetry into Grafana
-
🧰gdb, valgrind, sanitizers, delve, miri — a real debugging curriculum in every language
+
🧰gdb, valgrind, sanitizers, delve, miri: a debugging curriculum in every language
@@ -39,10 +39,10 @@ What this tutorial is
developers who already program and want to understand what their code
asks of the kernel. Each chapter builds the same runnable example
idiomatically in C++23, Go, and
- Rust — processes, files and I/O, memory, IPC,
+ Rust: processes, files and I/O, memory, IPC,
networking, and concurrency, then debugging, containers, observability,
and low-latency work. Everything runs locally on Fedora 44 except what
- genuinely needs root, kernel observation, or a second host — that runs in
+ needs root, kernel observation, or a second host; that runs in
a disposable KVM lab. eBPF appears as an observation toolkit
(bcc-tools, bpftrace, bpftool) aimed at your own programs; this is
systems programming, not kernel programming. All container images are
From 10aa9f3c422aa37f4c1f95121fedba92a6584c32 Mon Sep 17 00:00:00 2001
From: PatternCatalyst
Date: Sat, 31 Oct 2026 09:09:40 -0400
Subject: [PATCH 2/2] refactor(examples): idiomatic fixes (behavior-preserving)
+ recast comment dashes
Per-language idiomatic audit of the C++/Go/Rust/Lua/Python examples, behavior
preserving and build-verified where the toolchain is present:
- Go: signal.NotifyContext, net.JoinHostPort (vet IPv6), fixed an os.Exit that
skipped a deferred OTel shutdown (dropped final telemetry batch); go build ./...
passes all modules, go vet clean.
- Rust: unwrap->expect with invariants, manual loops->iterators, is_some_and;
cargo build passes all touched crates. Raw-syscall/unsafe demos left intact.
- C++: no idiom violations found (naked new/delete all legitimately scoped).
- Python: generate_diagram.py open() -> pathlib context managers; print() kept
where it is a CLI's actual output.
- Recast em dashes and ASCII ' -- ' in code comments across all five languages
(CLI flags, '-- ' separators, decrement operators, and output strings untouched).
---
.../04-syscalls-and-the-abi/cpp/src/main.cpp | 4 +--
examples/04-syscalls-and-the-abi/go/main.go | 4 +--
.../04-syscalls-and-the-abi/rust/src/main.rs | 4 +--
examples/04-syscalls-and-the-abi/verify.lua | 2 +-
.../05-errors-three-ways/cpp/src/main.cpp | 8 ++---
examples/05-errors-three-ways/go/main.go | 12 +++----
.../05-errors-three-ways/rust/src/main.rs | 10 +++---
.../cpp/src/main.cpp | 2 +-
examples/06-concurrency-three-ways/go/main.go | 2 +-
.../rust/src/main.rs | 2 +-
.../07-fds-raii-and-the-vfs/cpp/src/main.cpp | 2 +-
examples/07-fds-raii-and-the-vfs/go/main.go | 2 +-
.../07-fds-raii-and-the-vfs/rust/src/main.rs | 6 ++--
.../cpp/src/main.cpp | 2 +-
.../08-page-cache-and-durability/go/main.go | 2 +-
.../rust/src/main.rs | 5 +--
.../08-page-cache-and-durability/verify.lua | 2 +-
.../cpp/src/main.cpp | 14 ++++----
.../09-event-loops-epoll-inotify/go/main.go | 4 +--
.../rust/src/main.rs | 14 ++++----
.../09-event-loops-epoll-inotify/verify.lua | 2 +-
examples/10-io-uring/cpp/src/main.cpp | 2 +-
examples/10-io-uring/go/main.go | 4 +--
examples/10-io-uring/rust/src/main.rs | 6 ++--
examples/10-io-uring/verify.lua | 4 +--
.../11-process-lifecycle/cpp/src/main.cpp | 6 ++--
examples/11-process-lifecycle/go/main.go | 4 +--
.../11-process-lifecycle/rust/src/main.rs | 2 +-
examples/11-process-lifecycle/verify.lua | 2 +-
.../cpp/src/main.cpp | 4 +--
.../12-signals-and-safe-handling/go/main.go | 2 +-
.../rust/src/main.rs | 4 +--
.../cpp/src/main.cpp | 14 ++++----
.../13-pidfd-modern-process-mgmt/go/main.go | 12 +++----
.../rust/src/main.rs | 17 +++++----
.../13-pidfd-modern-process-mgmt/verify.lua | 6 ++--
.../cpp/src/main.cpp | 31 ++++++++--------
examples/14-identity-and-privilege/go/main.go | 6 ++--
.../rust/src/main.rs | 4 +--
examples/14-identity-and-privilege/verify.lua | 8 ++---
examples/15-virtual-memory/cpp/src/main.cpp | 2 +-
examples/15-virtual-memory/go/main.go | 2 +-
examples/15-virtual-memory/rust/src/main.rs | 20 +++++------
examples/15-virtual-memory/verify.lua | 2 +-
.../cpp/src/main.cpp | 6 ++--
.../16-mmap-and-shared-mappings/go/main.go | 6 ++--
.../rust/src/main.rs | 14 +++++---
.../16-mmap-and-shared-mappings/verify.lua | 2 +-
.../cpp/src/main.cpp | 6 ++--
.../17-allocators-and-gc-runtimes/go/main.go | 6 ++--
.../rust/src/main.rs | 14 +++-----
.../17-allocators-and-gc-runtimes/verify.lua | 2 +-
.../18-pipes-fifos-splice/cpp/src/main.cpp | 18 +++++-----
examples/18-pipes-fifos-splice/go/main.go | 18 +++++-----
.../18-pipes-fifos-splice/rust/src/main.rs | 25 +++++++------
examples/18-pipes-fifos-splice/verify.lua | 4 +--
.../cpp/src/main.cpp | 6 ++--
.../19-unix-sockets-fd-passing/go/main.go | 4 +--
.../rust/src/main.rs | 6 ++--
.../19-unix-sockets-fd-passing/verify.lua | 4 +--
.../cpp/src/main.cpp | 12 +++----
.../20-shared-memory-coordination/go/main.go | 6 ++--
.../rust/src/main.rs | 20 +++++++----
examples/21-tcp-sockets/cpp/src/main.cpp | 4 +--
examples/21-tcp-sockets/go/main.go | 6 ++--
examples/21-tcp-sockets/rust/src/main.rs | 4 +--
examples/21-tcp-sockets/verify.lua | 4 +--
.../22-scaling-the-server/cpp/src/main.cpp | 12 +++----
examples/22-scaling-the-server/go/main.go | 12 +++----
.../22-scaling-the-server/rust/src/main.rs | 12 +++----
examples/22-scaling-the-server/verify.lua | 14 ++++----
.../cpp/src/main.cpp | 6 ++--
examples/23-udp-and-peer-discovery/go/main.go | 6 ++--
.../rust/src/main.rs | 6 ++--
examples/23-udp-and-peer-discovery/verify.lua | 2 +-
.../24-time-timers-deadlines/cpp/src/main.cpp | 8 ++---
examples/24-time-timers-deadlines/go/main.go | 6 ++--
.../24-time-timers-deadlines/rust/src/main.rs | 12 +++----
.../cpp/src/main.cpp | 6 ++--
.../25-shared-state-and-the-futex/go/main.go | 4 +--
.../rust/src/main.rs | 12 +++----
.../25-shared-state-and-the-futex/verify.lua | 2 +-
.../26-atomics-and-lock-free/cpp/src/main.cpp | 2 +-
examples/26-atomics-and-lock-free/go/main.go | 4 +--
.../26-atomics-and-lock-free/rust/src/main.rs | 2 +-
.../cpp/src/main.cpp | 16 ++++-----
.../go/main.go | 8 ++---
.../rust/src/main.rs | 14 ++++----
.../cpp/src/main.cpp | 10 +++---
.../28-gdb-and-remote-debugging/go/main.go | 10 +++---
.../rust/src/main.rs | 10 +++---
.../28-gdb-and-remote-debugging/verify.lua | 8 ++---
.../cpp/src/main.cpp | 6 ++--
.../29-valgrind-sanitizers-miri/go/main.go | 8 ++---
.../rust/src/main.rs | 10 +++---
.../29-valgrind-sanitizers-miri/verify.lua | 16 ++++-----
.../cpp/src/main.cpp | 8 ++---
.../30-ebpf-observation-toolkit/go/main.go | 6 ++--
.../rust/src/main.rs | 8 ++---
.../30-ebpf-observation-toolkit/verify.lua | 16 ++++-----
.../cpp/src/main.cpp | 6 ++--
examples/31-per-language-toolbelts/go/main.go | 10 +++---
.../rust/src/main.rs | 6 ++--
examples/31-per-language-toolbelts/verify.lua | 4 +--
.../cpp/src/main.cpp | 18 +++++-----
examples/32-namespaces-and-cgroups/go/main.go | 30 ++++++++--------
.../rust/src/main.rs | 18 +++++-----
examples/32-namespaces-and-cgroups/verify.lua | 18 +++++-----
.../33-seccomp-and-landlock/cpp/src/main.cpp | 24 ++++++-------
examples/33-seccomp-and-landlock/go/main.go | 28 +++++++--------
.../33-seccomp-and-landlock/rust/src/main.rs | 26 +++++++-------
examples/33-seccomp-and-landlock/verify.lua | 10 +++---
.../cpp/src/main.cpp | 16 ++++-----
.../34-our-programs-in-containers/go/main.go | 14 ++++----
.../rust/src/main.rs | 16 ++++-----
.../34-our-programs-in-containers/verify.lua | 8 ++---
.../cpp/src/main.cpp | 6 ++--
examples/35-virtualization-and-kvm/go/main.go | 6 ++--
.../rust/src/main.rs | 6 ++--
examples/35-virtualization-and-kvm/verify.lua | 6 ++--
.../cpp/src/httpd.hpp | 2 +-
.../cpp/src/main.cpp | 6 ++--
.../cpp/src/procfs.cpp | 4 +--
.../36-proc-sys-and-the-agent/go/httpd.go | 12 +++----
examples/36-proc-sys-and-the-agent/go/main.go | 4 +--
.../36-proc-sys-and-the-agent/go/procfs.go | 4 +--
.../rust/src/httpd.rs | 2 +-
.../rust/src/main.rs | 4 +--
.../rust/src/procfs.rs | 4 +--
examples/36-proc-sys-and-the-agent/verify.lua | 4 +--
.../cpp/src/main.cpp | 20 +++++------
.../37-use-method-60s-analysis/go/main.go | 24 ++++++-------
.../rust/src/main.rs | 24 ++++++-------
.../37-use-method-60s-analysis/verify.lua | 32 ++++++++---------
.../38-opentelemetry-to-lgtm/cpp/src/main.cpp | 28 +++++++--------
.../cpp/src/telemetry.cpp | 2 +-
.../cpp/src/telemetry.hpp | 12 +++----
examples/38-opentelemetry-to-lgtm/go/main.go | 6 ++--
.../38-opentelemetry-to-lgtm/rust/src/main.rs | 20 +++++------
examples/38-opentelemetry-to-lgtm/verify.lua | 6 ++--
.../cpp/src/main.cpp | 16 ++++-----
.../39-benchmarking-without-lies/go/main.go | 18 +++++-----
.../rust/src/main.rs | 18 +++++-----
.../39-benchmarking-without-lies/verify.lua | 6 ++--
.../40-low-latency-fastpath/cpp/src/main.cpp | 16 ++++-----
examples/40-low-latency-fastpath/go/main.go | 18 +++++-----
.../40-low-latency-fastpath/rust/src/main.rs | 35 ++++++++++---------
examples/40-low-latency-fastpath/verify.lua | 32 ++++++++---------
examples/41-capstone-fleet/cpp/src/caps.cpp | 2 +-
examples/41-capstone-fleet/cpp/src/caps.hpp | 2 +-
.../41-capstone-fleet/cpp/src/chatterd.cpp | 10 +++---
.../41-capstone-fleet/cpp/src/chatterd.hpp | 2 +-
examples/41-capstone-fleet/cpp/src/fwatch.cpp | 14 ++++----
examples/41-capstone-fleet/cpp/src/fwatch.hpp | 2 +-
examples/41-capstone-fleet/cpp/src/main.cpp | 8 ++---
examples/41-capstone-fleet/cpp/src/pmon.cpp | 4 +--
examples/41-capstone-fleet/cpp/src/pmon.hpp | 2 +-
examples/41-capstone-fleet/cpp/src/proto.hpp | 2 +-
.../41-capstone-fleet/cpp/src/sysagent.cpp | 6 ++--
.../41-capstone-fleet/cpp/src/sysagent.hpp | 4 +--
.../41-capstone-fleet/cpp/src/telemetry.cpp | 6 ++--
.../41-capstone-fleet/cpp/src/telemetry.hpp | 10 +++---
examples/41-capstone-fleet/cpp/src/util.hpp | 4 +--
examples/41-capstone-fleet/go/caps.go | 4 +--
examples/41-capstone-fleet/go/chatterd.go | 6 ++--
examples/41-capstone-fleet/go/fwatch.go | 6 ++--
examples/41-capstone-fleet/go/main.go | 16 ++++++---
examples/41-capstone-fleet/go/pmon.go | 3 +-
examples/41-capstone-fleet/go/proto.go | 2 +-
examples/41-capstone-fleet/go/sysagent.go | 4 +--
examples/41-capstone-fleet/go/telemetry.go | 8 ++---
examples/41-capstone-fleet/go/util.go | 2 +-
examples/41-capstone-fleet/rust/src/caps.rs | 4 +--
.../41-capstone-fleet/rust/src/chatterd.rs | 12 +++----
examples/41-capstone-fleet/rust/src/fwatch.rs | 10 +++---
examples/41-capstone-fleet/rust/src/main.rs | 8 ++---
examples/41-capstone-fleet/rust/src/pmon.rs | 32 ++++++-----------
examples/41-capstone-fleet/rust/src/proto.rs | 2 +-
.../41-capstone-fleet/rust/src/sysagent.rs | 4 +--
.../41-capstone-fleet/rust/src/telemetry.rs | 16 ++++-----
examples/41-capstone-fleet/rust/src/util.rs | 4 +--
examples/41-capstone-fleet/verify.lua | 8 ++---
.../cpp/src/main.cpp | 16 ++++-----
examples/42-embedded-lua-scripting/go/main.go | 18 +++++-----
.../policy-broken.lua | 6 ++--
.../42-embedded-lua-scripting/policy-evil.lua | 6 ++--
.../42-embedded-lua-scripting/policy-v2.lua | 2 +-
examples/42-embedded-lua-scripting/policy.lua | 16 ++++-----
.../rust/src/main.rs | 14 ++++----
examples/42-embedded-lua-scripting/verify.lua | 2 +-
.../rust/src/main.rs | 6 ++--
.../rust/sysmacros/src/lib.rs | 4 +--
examples/44-go-runtime-for-systems/go/main.go | 12 +++----
examples/44-go-runtime-for-systems/verify.lua | 2 +-
examples/45-linux-analysis-suites/verify.lua | 14 ++++----
.../46-cpp-toolbox/cpp/conan/conanfile.py | 2 +-
.../46-cpp-toolbox/cpp/conan/src/main.cpp | 2 +-
.../cpp/fixtures/misformatted.cpp | 2 +-
examples/46-cpp-toolbox/cpp/src/digest.hpp | 2 +-
examples/46-cpp-toolbox/cpp/src/smell.hpp | 4 +--
examples/46-cpp-toolbox/cpp/src/toolbox.cpp | 8 ++---
.../46-cpp-toolbox/cpp/toolbox-printers.py | 4 +--
examples/46-cpp-toolbox/verify.lua | 10 +++---
examples/47-go-toolbox/go/bench_test.go | 2 +-
examples/47-go-toolbox/go/digest.go | 4 +--
.../47-go-toolbox/go/internal/gen/main.go | 8 ++---
examples/47-go-toolbox/go/main.go | 10 +++---
examples/47-go-toolbox/go/smells.go | 4 +--
examples/47-go-toolbox/verify.lua | 8 ++---
.../rust/fixtures/misformatted.rs | 2 +-
examples/48-rust-toolbox/rust/src/digest.rs | 4 +--
examples/48-rust-toolbox/rust/src/lintbait.rs | 2 +-
examples/48-rust-toolbox/rust/src/main.rs | 4 +--
examples/48-rust-toolbox/verify.lua | 10 +++---
.../cpp/src/conc.cpp | 24 ++++++-------
.../cpp/src/contracts/contracts_demo.cpp | 4 +--
.../cpp/src/hardening/oob.cpp | 6 ++--
.../49-concurrency-vs-parallelism/verify.lua | 22 ++++++------
examples/50-pthreads/cpp/src/pthreads.cpp | 20 +++++------
examples/50-pthreads/verify.lua | 8 ++---
.../51-std-threading/cpp/src/stdthread.cpp | 14 ++++----
examples/51-std-threading/verify.lua | 8 ++---
.../52-boost-thread/cpp/src/boostthread.cpp | 32 ++++++++---------
examples/52-boost-thread/verify.lua | 18 +++++-----
examples/53-coroutines/cpp/src/coro.cpp | 22 ++++++------
examples/53-coroutines/verify.lua | 12 +++----
examples/54-boost-fiber/cpp/src/fiberdemo.cpp | 18 +++++-----
examples/54-boost-fiber/verify.lua | 14 ++++----
examples/55-boost-asio/cpp/src/asiodemo.cpp | 24 ++++++-------
examples/55-boost-asio/verify.lua | 8 ++---
.../cpp/conan/conanfile.py | 2 +-
.../cpp/conan/recipe/conanfile.py | 2 +-
.../cpp/src/capstone.cpp | 12 +++----
.../cpp/src/senders.cpp | 8 ++---
.../cpp/src/workload.hpp | 6 ++--
examples/56-capstone-comparison/verify.lua | 8 ++---
scripts/generate_diagram.py | 11 +++---
scripts/test-all-examples.py | 2 +-
scripts/validate.py | 2 +-
239 files changed, 1077 insertions(+), 1072 deletions(-)
diff --git a/examples/04-syscalls-and-the-abi/cpp/src/main.cpp b/examples/04-syscalls-and-the-abi/cpp/src/main.cpp
index 1f53973..a31a130 100644
--- a/examples/04-syscalls-and-the-abi/cpp/src/main.cpp
+++ b/examples/04-syscalls-and-the-abi/cpp/src/main.cpp
@@ -1,5 +1,5 @@
-// sysprobe: a labeled syscall specimen — openat+write+close of an unlinked
-// temp file, a 10 ms nanosleep, and 16 bytes of getrandom — built to be
+// sysprobe: a labeled syscall specimen (openat+write+close of an unlinked
+// temp file, a 10 ms nanosleep, and 16 bytes of getrandom), built to be
// watched under strace(1). Prints "step= ok" per step, then a summary.
#include
diff --git a/examples/04-syscalls-and-the-abi/go/main.go b/examples/04-syscalls-and-the-abi/go/main.go
index 1a0ad67..3220693 100644
--- a/examples/04-syscalls-and-the-abi/go/main.go
+++ b/examples/04-syscalls-and-the-abi/go/main.go
@@ -1,5 +1,5 @@
-// sysprobe: a labeled syscall specimen — openat+write+close of an unlinked
-// temp file, a 10 ms nanosleep, and 16 bytes of getrandom — built to be
+// sysprobe: a labeled syscall specimen (openat+write+close of an unlinked
+// temp file, a 10 ms nanosleep, and 16 bytes of getrandom), built to be
// watched under strace(1). Prints "step= ok" per step, then a summary.
package main
diff --git a/examples/04-syscalls-and-the-abi/rust/src/main.rs b/examples/04-syscalls-and-the-abi/rust/src/main.rs
index 00bedf9..17f86c8 100644
--- a/examples/04-syscalls-and-the-abi/rust/src/main.rs
+++ b/examples/04-syscalls-and-the-abi/rust/src/main.rs
@@ -1,5 +1,5 @@
-//! sysprobe: a labeled syscall specimen — openat+write+close of an unlinked
-//! temp file, a 10 ms nanosleep, and 16 bytes of getrandom — built to be
+//! sysprobe: a labeled syscall specimen (openat+write+close of an unlinked
+//! temp file, a 10 ms nanosleep, and 16 bytes of getrandom), built to be
//! watched under strace(1). Prints "step= ok" per step, then a summary.
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
diff --git a/examples/04-syscalls-and-the-abi/verify.lua b/examples/04-syscalls-and-the-abi/verify.lua
index 0da0f28..0e2c18e 100644
--- a/examples/04-syscalls-and-the-abi/verify.lua
+++ b/examples/04-syscalls-and-the-abi/verify.lua
@@ -1,5 +1,5 @@
-- Verify sysprobe: run the demo for LSP_LANG and assert the labeled syscall
--- sequence — four "step= ok" lines plus the summary, in order.
+-- sequence: four "step= ok" lines plus the summary, in order.
-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and EXAMPLE_DIR set.
local script_dir = arg[0]:match("(.*/)") or "./"
diff --git a/examples/05-errors-three-ways/cpp/src/main.cpp b/examples/05-errors-three-ways/cpp/src/main.cpp
index abfba7f..a2bb698 100644
--- a/examples/05-errors-three-ways/cpp/src/main.cpp
+++ b/examples/05-errors-three-ways/cpp/src/main.cpp
@@ -1,8 +1,8 @@
-// copyx SRC DST — file copier that establishes the book's error taxonomy.
+// copyx SRC DST: file copier that establishes the book's error taxonomy.
//
-// exit 0 — success; "copied bytes" on stdout
-// exit 2 — source-side failure (open/read SRC); "copyx: " on stderr
-// exit 3 — destination-side failure (open/write/close DST); same shape
+// exit 0: success; "copied bytes" on stdout
+// exit 2: source-side failure (open/read SRC); "copyx: " on stderr
+// exit 3: destination-side failure (open/write/close DST); same shape
//
// EINTR on read(2)/write(2) is retried (the syscall was interrupted before
// completing; reissuing it is the only correct policy), and short writes are
diff --git a/examples/05-errors-three-ways/go/main.go b/examples/05-errors-three-ways/go/main.go
index 55c8006..f07d4f0 100644
--- a/examples/05-errors-three-ways/go/main.go
+++ b/examples/05-errors-three-ways/go/main.go
@@ -1,8 +1,8 @@
-// copyx SRC DST — file copier that establishes the book's error taxonomy.
+// copyx SRC DST: file copier that establishes the book's error taxonomy.
//
-// exit 0 — success; "copied bytes" on stdout
-// exit 2 — source-side failure (open/read SRC); "copyx: " on stderr
-// exit 3 — destination-side failure (open/write/close DST); same shape
+// exit 0: success; "copied bytes" on stdout
+// exit 2: source-side failure (open/read SRC); "copyx: " on stderr
+// exit 3: destination-side failure (open/write/close DST); same shape
//
// EINTR on read(2)/write(2) is retried (the syscall was interrupted before
// completing; reissuing it is the only correct policy), and short writes are
@@ -46,8 +46,8 @@ func newOpError(op, path string, err error, exit int) *opError {
return &opError{op: op, path: path, err: errno, exit: exit}
}
-// strerror renders an errno the way glibc's strerror(3) does — first letter
-// capitalized — so all three implementations print identical reason text.
+// strerror renders an errno the way glibc's strerror(3) does, first letter
+// capitalized, so all three implementations print identical reason text.
func strerror(errno unix.Errno) string {
r := []rune(errno.Error())
if len(r) > 0 {
diff --git a/examples/05-errors-three-ways/rust/src/main.rs b/examples/05-errors-three-ways/rust/src/main.rs
index b1f9658..1b2f028 100644
--- a/examples/05-errors-three-ways/rust/src/main.rs
+++ b/examples/05-errors-three-ways/rust/src/main.rs
@@ -1,8 +1,8 @@
-//! copyx SRC DST — file copier that establishes the book's error taxonomy.
+//! copyx SRC DST: file copier that establishes the book's error taxonomy.
//!
-//! exit 0 — success; "copied bytes" on stdout
-//! exit 2 — source-side failure (open/read SRC); "copyx: " on stderr
-//! exit 3 — destination-side failure (open/write/close DST); same shape
+//! exit 0: success; "copied bytes" on stdout
+//! exit 2: source-side failure (open/read SRC); "copyx: " on stderr
+//! exit 3: destination-side failure (open/write/close DST); same shape
//!
//! EINTR on read(2)/write(2) is retried (the syscall was interrupted before
//! completing; reissuing it is the only correct policy), and short writes are
@@ -42,7 +42,7 @@ impl CopyError {
/// Render an errno the way glibc's strerror(3) does, so all three
/// implementations print identical reason text. `Errno`'s own Display goes
-/// through `std::io::Error`, which appends " (os error N)" — strip it.
+/// through `std::io::Error`, which appends " (os error N)"; strip it.
fn strerror(errno: Errno) -> String {
let s = std::io::Error::from(errno).to_string();
match s.rfind(" (os error ") {
diff --git a/examples/06-concurrency-three-ways/cpp/src/main.cpp b/examples/06-concurrency-three-ways/cpp/src/main.cpp
index 6d6e02e..517a073 100644
--- a/examples/06-concurrency-three-ways/cpp/src/main.cpp
+++ b/examples/06-concurrency-three-ways/cpp/src/main.cpp
@@ -1,4 +1,4 @@
-// parhash: parallel FNV-1a 64 checksummer — C++23 flavor.
+// parhash: parallel FNV-1a 64 checksummer: C++23 flavor.
//
// Concurrency shape: 4 std::jthread workers pull paths from a mutex-guarded
// queue (condition_variable_any so waits are stop_token-aware). A dedicated
diff --git a/examples/06-concurrency-three-ways/go/main.go b/examples/06-concurrency-three-ways/go/main.go
index a978486..69109c6 100644
--- a/examples/06-concurrency-three-ways/go/main.go
+++ b/examples/06-concurrency-three-ways/go/main.go
@@ -1,4 +1,4 @@
-// parhash: parallel FNV-1a 64 checksummer — Go flavor.
+// parhash: parallel FNV-1a 64 checksummer, Go flavor.
//
// Concurrency shape: 4 worker goroutines receive relative paths from an
// unbuffered channel fed by a walker goroutine; results flow back on a second
diff --git a/examples/06-concurrency-three-ways/rust/src/main.rs b/examples/06-concurrency-three-ways/rust/src/main.rs
index bab4771..df1f62d 100644
--- a/examples/06-concurrency-three-ways/rust/src/main.rs
+++ b/examples/06-concurrency-three-ways/rust/src/main.rs
@@ -1,4 +1,4 @@
-// parhash: parallel FNV-1a 64 checksummer — Rust flavor.
+// parhash: parallel FNV-1a 64 checksummer, Rust flavor.
//
// Concurrency shape: 4 scoped threads share one mpsc Receiver behind a Mutex
// and pull relative paths from it; results flow back to the main thread on a
diff --git a/examples/07-fds-raii-and-the-vfs/cpp/src/main.cpp b/examples/07-fds-raii-and-the-vfs/cpp/src/main.cpp
index 53bfa80..4cbe969 100644
--- a/examples/07-fds-raii-and-the-vfs/cpp/src/main.cpp
+++ b/examples/07-fds-raii-and-the-vfs/cpp/src/main.cpp
@@ -1,4 +1,4 @@
-// fwatch v0 — snapshot/diff a directory tree with dirfd-relative syscalls.
+// fwatch v0: snapshot/diff a directory tree with dirfd-relative syscalls.
//
// fwatch snapshot DIR one " " line per
// regular file, sorted by path
diff --git a/examples/07-fds-raii-and-the-vfs/go/main.go b/examples/07-fds-raii-and-the-vfs/go/main.go
index 8040b5d..17d6cbf 100644
--- a/examples/07-fds-raii-and-the-vfs/go/main.go
+++ b/examples/07-fds-raii-and-the-vfs/go/main.go
@@ -1,4 +1,4 @@
-// fwatch v0 — snapshot/diff a directory tree with dirfd-relative syscalls.
+// fwatch v0: snapshot/diff a directory tree with dirfd-relative syscalls.
//
// fwatch snapshot DIR one " " line per
// regular file, sorted by path
diff --git a/examples/07-fds-raii-and-the-vfs/rust/src/main.rs b/examples/07-fds-raii-and-the-vfs/rust/src/main.rs
index e35edc1..4b791d5 100644
--- a/examples/07-fds-raii-and-the-vfs/rust/src/main.rs
+++ b/examples/07-fds-raii-and-the-vfs/rust/src/main.rs
@@ -1,4 +1,4 @@
-// fwatch v0 — snapshot/diff a directory tree with dirfd-relative syscalls.
+// fwatch v0: snapshot/diff a directory tree with dirfd-relative syscalls.
//
// fwatch snapshot DIR one " " line per
// regular file, sorted by path
@@ -7,7 +7,7 @@
// summary
//
// The walk is deliberately explicit: rustix::fs::openat relative to the
-// parent directory fd (an OwnedFd — closed exactly once, on drop),
+// parent directory fd (an OwnedFd, closed exactly once, on drop),
// rustix::fs::Dir to enumerate, and rustix::fs::statat with
// SYMLINK_NOFOLLOW to classify entries.
@@ -24,7 +24,7 @@ struct Info {
mtime: i64,
}
-/// Sorted by path, bytewise — BTreeMap keeps snapshot output deterministic.
+/// Sorted by path, bytewise: BTreeMap keeps snapshot output deterministic.
type Tree = BTreeMap;
const DIR_FLAGS: OFlags = OFlags::RDONLY
diff --git a/examples/08-page-cache-and-durability/cpp/src/main.cpp b/examples/08-page-cache-and-durability/cpp/src/main.cpp
index 38d16f9..366e6b6 100644
--- a/examples/08-page-cache-and-durability/cpp/src/main.cpp
+++ b/examples/08-page-cache-and-durability/cpp/src/main.cpp
@@ -1,4 +1,4 @@
-// iobench: page cache vs durability — buffered writes, periodic fdatasync(2),
+// iobench: page cache vs durability: buffered writes, periodic fdatasync(2),
// and O_DIRECT, with identical CLI and output across the three languages.
//
// iobench --mode buffered|fsync-every|direct [--every N] [--size-mb M] FILE
diff --git a/examples/08-page-cache-and-durability/go/main.go b/examples/08-page-cache-and-durability/go/main.go
index 6697b2d..60ea894 100644
--- a/examples/08-page-cache-and-durability/go/main.go
+++ b/examples/08-page-cache-and-durability/go/main.go
@@ -1,4 +1,4 @@
-// iobench: page cache vs durability — buffered writes, periodic fdatasync(2),
+// iobench: page cache vs durability: buffered writes, periodic fdatasync(2),
// and O_DIRECT, with identical CLI and output across the three languages.
//
// iobench --mode buffered|fsync-every|direct [--every N] [--size-mb M] FILE
diff --git a/examples/08-page-cache-and-durability/rust/src/main.rs b/examples/08-page-cache-and-durability/rust/src/main.rs
index 5f10bb3..6c423ef 100644
--- a/examples/08-page-cache-and-durability/rust/src/main.rs
+++ b/examples/08-page-cache-and-durability/rust/src/main.rs
@@ -1,5 +1,6 @@
-// iobench: page cache vs durability — buffered writes, periodic fdatasync(2),
-// and O_DIRECT, with identical CLI and output across the three languages.
+// iobench: page cache vs durability, comparing buffered writes, periodic
+// fdatasync(2), and O_DIRECT, with identical CLI and output across the three
+// languages.
//
// iobench --mode buffered|fsync-every|direct [--every N] [--size-mb M] FILE
//
diff --git a/examples/08-page-cache-and-durability/verify.lua b/examples/08-page-cache-and-durability/verify.lua
index b0389dd..2772434 100644
--- a/examples/08-page-cache-and-durability/verify.lua
+++ b/examples/08-page-cache-and-durability/verify.lua
@@ -1,5 +1,5 @@
-- Verify 08-page-cache-and-durability: run iobench in buffered, fsync-every,
--- and direct modes and assert the observable behavior — report line shapes,
+-- and direct modes and assert the observable behavior: report line shapes,
-- exact byte counts, the on-disk file size (stat), and exit codes. Timing
-- values are asserted for shape only (numbers flake).
-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and EXAMPLE_DIR set.
diff --git a/examples/09-event-loops-epoll-inotify/cpp/src/main.cpp b/examples/09-event-loops-epoll-inotify/cpp/src/main.cpp
index f8ca463..6269083 100644
--- a/examples/09-event-loops-epoll-inotify/cpp/src/main.cpp
+++ b/examples/09-event-loops-epoll-inotify/cpp/src/main.cpp
@@ -1,11 +1,11 @@
-// fwatch v1 — a file watcher growing chapter by chapter.
+// fwatch v1: a file watcher growing chapter by chapter.
//
// v0 subcommands (snapshot/diff) still work; v1 adds `watch`, built as ONE
// epoll loop over three kernel file descriptors on a single thread:
//
-// inotify fd — filesystem events for the watched directory
-// timerfd — the debounce (100 ms per path) AND the overall timeout
-// signalfd — SIGINT/SIGTERM delivered as ordinary readable data
+// inotify fd: filesystem events for the watched directory
+// timerfd: the debounce (100 ms per path) AND the overall timeout
+// signalfd: SIGINT/SIGTERM delivered as ordinary readable data
//
// The readiness model is explicit: nothing here blocks except epoll_wait(2).
@@ -51,7 +51,7 @@ constexpr auto kDebounce = milliseconds{100};
}
// ---------------------------------------------------------------------------
-// RAII file descriptor — the only owner of every fd in this program.
+// RAII file descriptor: the only owner of every fd in this program.
// ---------------------------------------------------------------------------
class Fd {
@@ -193,7 +193,7 @@ int cmd_diff(const std::string& old_path, const std::string& new_path) {
}
// ---------------------------------------------------------------------------
-// v1: watch — one epoll loop, three fds, zero threads beyond main.
+// v1: watch: one epoll loop, three fds, zero threads beyond main.
// ---------------------------------------------------------------------------
enum class Kind : std::uint8_t { created, modified, deleted };
@@ -329,7 +329,7 @@ int cmd_watch(const std::string& dir, int timeout_ms) {
for (;;) {
// One timer covers both jobs: it is always armed to whichever comes
- // first — the overall timeout or the earliest per-path debounce.
+ // first: the overall timeout or the earliest per-path debounce.
auto next = deadline;
for (const auto& [_, p] : pending) {
next = std::min(next, p.due);
diff --git a/examples/09-event-loops-epoll-inotify/go/main.go b/examples/09-event-loops-epoll-inotify/go/main.go
index d87fa83..45a9715 100644
--- a/examples/09-event-loops-epoll-inotify/go/main.go
+++ b/examples/09-event-loops-epoll-inotify/go/main.go
@@ -1,4 +1,4 @@
-// fwatch v1 — a file watcher growing chapter by chapter.
+// fwatch v1: a file watcher growing chapter by chapter.
//
// v0 subcommands (snapshot/diff) still work; v1 adds `watch`. Where the C++
// and Rust versions run one explicit epoll loop over inotify + timerfd +
@@ -114,7 +114,7 @@ func cmdDiff(oldPath, newPath string) error {
}
// ---------------------------------------------------------------------------
-// v1: watch — goroutine + channels; the runtime owns the epoll loop.
+// v1 (watch): goroutine + channels; the runtime owns the epoll loop.
// ---------------------------------------------------------------------------
type fsEvent struct {
diff --git a/examples/09-event-loops-epoll-inotify/rust/src/main.rs b/examples/09-event-loops-epoll-inotify/rust/src/main.rs
index dd800d4..93d99e2 100644
--- a/examples/09-event-loops-epoll-inotify/rust/src/main.rs
+++ b/examples/09-event-loops-epoll-inotify/rust/src/main.rs
@@ -1,12 +1,12 @@
-// fwatch v1 — a file watcher growing chapter by chapter.
+// fwatch v1: a file watcher growing chapter by chapter.
//
// v0 subcommands (snapshot/diff) still work; v1 adds `watch`, built exactly
// like the C++ version: ONE epoll loop over three kernel file descriptors on
-// a single thread —
+// a single thread:
//
-// inotify fd — filesystem events for the watched directory
-// timerfd — the debounce (100 ms per path) AND the overall timeout
-// signalfd — SIGINT/SIGTERM delivered as ordinary readable data
+// inotify fd : filesystem events for the watched directory
+// timerfd : the debounce (100 ms per path) AND the overall timeout
+// signalfd : SIGINT/SIGTERM delivered as ordinary readable data
//
// nix wraps each fd in an owning type (OwnedFd underneath), so every resource
// closes on drop; the readiness model stays explicit.
@@ -88,7 +88,7 @@ fn cmd_diff(old_path: &str, new_path: &str) -> Result<()> {
}
// ---------------------------------------------------------------------------
-// v1: watch — one epoll loop, three fds, zero threads beyond main.
+// v1 watch: one epoll loop, three fds, zero threads beyond main.
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -198,7 +198,7 @@ fn cmd_watch(dir: &str, timeout_ms: u64) -> Result<()> {
loop {
// One timer covers both jobs: it is always armed to whichever comes
- // first — the overall timeout or the earliest per-path debounce.
+ // first: the overall timeout or the earliest per-path debounce.
let next = pending.values().map(|p| p.due).min().map_or(deadline, |d| d.min(deadline));
arm_timer(&timer, next.saturating_duration_since(Instant::now()))?;
diff --git a/examples/09-event-loops-epoll-inotify/verify.lua b/examples/09-event-loops-epoll-inotify/verify.lua
index f183ffd..7fc039e 100644
--- a/examples/09-event-loops-epoll-inotify/verify.lua
+++ b/examples/09-event-loops-epoll-inotify/verify.lua
@@ -1,7 +1,7 @@
-- Verify fwatch v1 (09-event-loops-epoll-inotify) for LSP_LANG.
--
-- Asserts observable behavior, identical across cpp/go/rust:
--- 1. v0 regression — snapshot format and diff created/modified/deleted lines
+-- 1. v0 regression: snapshot format and diff created/modified/deleted lines
-- 2. watch: background run, mutate the directory after ~400 ms, expect one
-- debounced event line per path and a clean "(timeout)" exit, exit 0
-- 3. watch: SIGTERM produces the "(signal)" exit line, exit 0
diff --git a/examples/10-io-uring/cpp/src/main.cpp b/examples/10-io-uring/cpp/src/main.cpp
index 61465e3..64ad6d8 100644
--- a/examples/10-io-uring/cpp/src/main.cpp
+++ b/examples/10-io-uring/cpp/src/main.cpp
@@ -1,4 +1,4 @@
-// fwatch v2 — batched sync engine (chapter 10: io_uring).
+// fwatch v2: batched sync engine (chapter 10: io_uring).
//
// Subcommands:
// fwatch scan DIR (v0: polling snapshot)
diff --git a/examples/10-io-uring/go/main.go b/examples/10-io-uring/go/main.go
index 25bb54b..47277c4 100644
--- a/examples/10-io-uring/go/main.go
+++ b/examples/10-io-uring/go/main.go
@@ -1,4 +1,4 @@
-// fwatch v2 — batched sync engine (chapter 10: io_uring).
+// fwatch v2: batched sync engine (chapter 10: io_uring).
//
// Subcommands:
//
@@ -9,7 +9,7 @@
// Go has no supported io_uring story: the runtime already multiplexes
// goroutines over epoll in the netpoller, and the io_uring proposal
// (golang/go#31908) has been on hold for years. So the uring engine is
-// deliberately absent here — "fwatch sync ... --engine uring" reports it
+// deliberately absent here: "fwatch sync ... --engine uring" reports it
// unsupported and exits 64. That asymmetry IS the chapter's point.
package main
diff --git a/examples/10-io-uring/rust/src/main.rs b/examples/10-io-uring/rust/src/main.rs
index c0a66f3..71e825c 100644
--- a/examples/10-io-uring/rust/src/main.rs
+++ b/examples/10-io-uring/rust/src/main.rs
@@ -1,4 +1,4 @@
-// fwatch v2 — batched sync engine (chapter 10: io_uring).
+// fwatch v2: batched sync engine (chapter 10: io_uring).
//
// Subcommands:
// fwatch scan DIR (v0: polling snapshot)
@@ -102,8 +102,8 @@ fn copy_file_uring(ring: &mut IoUring, src: &Path, dst: &Path) -> Result {
let len = CHUNK.min(size - off) as u32;
lens[pairs] = len;
let buf = bufs[pairs].as_mut_ptr();
- // READ this chunk into the pair's buffer, then — only if it
- // fully succeeded (IO_LINK) — WRITE it at the same offset.
+ // READ this chunk into the pair's buffer, then, only if it
+ // fully succeeded (IO_LINK), WRITE it at the same offset.
let read = opcode::Read::new(types::Fd(input.as_raw_fd()), buf, len)
.offset(off)
.build()
diff --git a/examples/10-io-uring/verify.lua b/examples/10-io-uring/verify.lua
index da9be6f..ee3f948 100644
--- a/examples/10-io-uring/verify.lua
+++ b/examples/10-io-uring/verify.lua
@@ -1,5 +1,5 @@
-- Verify fwatch v2: build a fixture tree, then assert observable behavior of
--- every subcommand — exact scan/sync counters, byte-equal copies (diff -r),
+-- every subcommand: exact scan/sync counters, byte-equal copies (diff -r),
-- inotify event lines from watch, and the Go uring refusal (exit 64).
-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and EXAMPLE_DIR set.
@@ -42,7 +42,7 @@ r = demo("scan " .. src)
checks.expect_exit(r, 0, lang .. ": scan exits 0")
checks.expect_match(r.out, "scanned 5 files 1348634 bytes", lang .. ": scan totals")
--- v2: sync with the rw engine — counters plus a byte-identical tree.
+-- v2: sync with the rw engine (counters plus a byte-identical tree).
local dst_rw = ws .. "/dst-rw"
r = demo("sync " .. src .. " " .. dst_rw .. " --engine rw")
checks.expect_exit(r, 0, lang .. ": sync rw exits 0")
diff --git a/examples/11-process-lifecycle/cpp/src/main.cpp b/examples/11-process-lifecycle/cpp/src/main.cpp
index e22b75e..08aa68d 100644
--- a/examples/11-process-lifecycle/cpp/src/main.cpp
+++ b/examples/11-process-lifecycle/cpp/src/main.cpp
@@ -1,4 +1,4 @@
-// pmon run -- CMD [ARGS...] — minimal process monitor for the process
+// pmon run -- CMD [ARGS...]: minimal process monitor for the process
// lifecycle chapter: fork(2) + execvp(3) the command, waitpid(2) for it,
// then report its fate and resource usage.
//
@@ -11,8 +11,8 @@
// exits 127 with no report lines (there was no child run to report on).
//
// The chapter discusses posix_spawn(3); fork/exec is used here because the
-// two-step model — and the CLOEXEC self-pipe that reports exec failure back
-// to the parent — is exactly what the chapter teaches.
+// two-step model (and the CLOEXEC self-pipe that reports exec failure back
+// to the parent) is exactly what the chapter teaches.
#include
#include