Files
vdm/core/docs/buffer-sizing.md
T
samiandClaude Sonnet 5 c13b5dff21 core: answer PROTO's bufferBytes range question; accept inclusive endByte
buffer-sizing.md: the frozen 4 KiB–8 MiB and docs/04's 64 KiB–64 MiB both
miss. Recommend 64 KiB – 16 MiB, default 2 MiB, max_total_buffer_bytes
unchanged at 256 MiB:
 - 4 KiB floor is smaller than one libcurl write callback -> a syscall per
   chunk; 64 KiB is the smallest floor that coalesces.
 - throughput vs write size is flat past ~8 MiB on NVMe; 8–16 MiB is
   disk-stall absorption headroom for the fast-pipe/slow-disk case; 64 MiB
   is cache pressure for zero gain.
 - 32 segments x 64 MiB = 2 GiB vs the 256 MiB cap means the docs/04 max is
   unreachable past 4 total active segments — a misleading Options value.
   16 MiB is reachable for single-/light-multitask and clamps to 8 MiB
   under heavy parallelism, which is correct.
 - default 4 MiB x 20 downloads = 80 MiB, busting the "<=60 MB RSS / 20
   downloads" DoD; 2 MiB fits. Filed as request B4.

proto-requests-m1.md: B3 endByte accepted as inclusive (HTTP Range
semantics, no curl-boundary off-by-one); [start,end) ask withdrawn; stage
6 designed against inclusive. New B3a: the Content-Length: 0 whole-file
case needs a representable zero-length segment — min_segment_bytes means
CORE never makes empty segments mid-download, so it's only the degenerate
case; mild preference for startByte+length over an endByte=startByte-1
sentinel.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
2026-09-09 19:57:46 +04:00

117 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CORE → PROTO — `bufferBytes` range (answer to the freeze question)
`contracts/` froze `bufferBytes` at **4 KiB 8 MiB**; `docs/04` §4 line 70 says
**64 KiB 64 MiB, default 4 MiB**. They disagree on the floor (16×), the ceiling (8×),
and the RSS budget can't hold the default. CORE owns the ring buffer, the 32-segment
ceiling and `max_total_buffer_bytes`, so here is the range that is actually right and why.
PROTO to land schema + `docs/04` §4 + an ADR together.
## Recommendation
| Field | Value | |
|---|---|---|
| `bufferBytes` minimum | **65536** (64 KiB) | from `docs/04`; the frozen 4 KiB is wrong — see below |
| `bufferBytes` maximum | **16777216** (16 MiB) | 8 MiB is defensible if simplicity wins; 64 MiB is not |
| `bufferBytes` default | **2097152** (2 MiB) | 4 MiB busts the RSS DoD — see below |
| `max_total_buffer_bytes` default | **268435456** (256 MiB), unchanged | the real ceiling; the backstop for everything |
## Why the floor is 64 KiB, not 4 KiB
The buffer's first job is to coalesce libcurl write-callback deliveries into one `pwrite`.
Over HTTP/2 a single write callback is routinely 1664 KiB, and can be up to ~256 KiB. A
4 KiB ring buffer is **smaller than one callback**: every callback would have to flush
mid-call (or loop), so the "one `pwrite` per fill" design degrades to a syscall per curl
chunk — the exact thing the buffer exists to avoid. 64 KiB (≈4 typical chunks) is the
smallest floor that still buys anything. 4 KiB is a page size that wandered into a
throughput knob.
## Why the ceiling is ~16 MiB, not 64 MiB
Two things the buffer buys, past coalescing:
1. **Large sequential writes.** On NVMe, write throughput as a function of write size is
flat by ~14 MiB. From 4 MiB to 8 MiB you gain a little on syscall overhead at
multi-Gbit; past 8 MiB there is **no throughput left to get** — you are only adding
`fdatasync` latency and page-cache pressure (a 40 GB ISO must not evict the user's
working set — `docs/04` §4).
2. **Absorbing a disk stall without stalling the socket.** This is the only reason to go
above 8 MiB. Fast link + bursty storage (HDD, SMR, USB, a network mount): 1 Gbit is
~125 MB/s, so 16 MiB per segment ≈ 130 ms of write-stall cover; across 48 segments,
~0.51 s aggregate — enough to ride out a seek storm. Beyond 16 MiB the marginal cover
isn't worth the cache footprint.
So: **8 MiB captures all the throughput; 816 MiB is stall-absorption headroom for the
fast-pipe/slow-disk power user; >16 MiB is waste.**
## The arithmetic you asked about: 32 × 64 MiB vs a 256 MiB cap
`max_total_buffer_bytes` (256 MiB) caps the sum of every active segment's buffer across
every active task. The effective per-segment buffer is
```
effective = clamp( requested,
64 KiB,
floor(max_total_buffer_bytes / active_segment_count) )
```
Reachable `bufferBytes` before the cap clamps it, by workload:
| Active work | segments | cap ÷ segments | 16 MiB reachable? |
|---|---|---|---|
| 1 task, 1 seg (small file / non-resumable) | 1 | 256 MiB | yes |
| 1 task, 8 seg (typical big download) | 8 | 32 MiB | yes, unclamped |
| 1 task, 16 seg | 16 | 16 MiB | exactly at the cap |
| 1 task, 32 seg | 32 | 8 MiB | **clamps to 8 MiB** |
| 4 tasks × 8 seg | 32 | 8 MiB | clamps to 8 MiB |
Now the same table with a **64 MiB** ceiling: it is unreachable the moment total active
segments exceed **4** (256 / 64). At the default 8 segments the user asks for 64 MiB and
silently gets 32 MiB; at 32 segments they get 8 MiB. A maximum that no realistic
configuration can actually use is a misleading number in the Options dialog. 16 MiB is
reachable for the single-task and light-multitask cases — the cases where deep buffering
is the point — and degrades predictably (to 8 MiB) exactly when per-segment buffering
stops mattering because each segment is only getting 1/32 of the link.
32 segments is already past the point of diminishing returns on segment count itself
(`docs/04` §3: "more segments than [1 MiB each] is pure overhead and gets you
rate-limited"); clamping their buffers to 8 MiB is the right behaviour, not a regression.
## The default: 4 MiB fails the RSS DoD
`docs/04` §8: "≤ 60 MB RSS with 20 active downloads at default buffers." Twenty active
downloads, each with at least one segment:
- default **4 MiB** → 20 × 4 = **80 MiB in buffers alone**, before curl handles, TLS
buffers, thread stacks, and the task table. Busts 60 MB outright. The 256 MiB global
cap does not save you — 80 < 256, so nothing clamps.
- default **2 MiB** → 20 × 2 = 40 MiB, leaving ~20 MiB for everything else. Fits.
- A single 8-segment download at 2 MiB is 16 MiB of buffer — already ample for line rate
on NVMe (see the throughput-plateau point above).
So the default has to be **2 MiB** for the RSS target and the buffer default to be
consistent, or `docs/04` §8 has to be renegotiated. 2 MiB is the cheaper fix and is not a
throughput compromise.
## Clamp behaviour CORE will implement
- Per **task start** and on any change to the active-segment count (new task, task
finishing, a steal), recompute `effective` for every live segment by the formula above.
- Never clamp below the 64 KiB floor. If `max_total_buffer_bytes / active_segment_count`
is itself below 64 KiB (would need >4096 concurrent segments — not reachable at the
32-per-task ceiling and a sane concurrent-task limit), CORE admits fewer concurrent
segments rather than shipping a sub-floor buffer.
- The resulting `effective` value is what CORE reports upward for the readback field
(request **B2a**). `requested` is echoed back too so the GUI can show "8 MiB (using
2 MiB)".
## Net contract delta for PROTO
- `DownloadSpec.bufferBytes`, `download.update` patch: `minimum: 65536`, `maximum:
16777216`, `default: 2097152`.
- `docs/04` §4 line ~70: "Default 2 MiB, range 64 KiB 16 MiB. Silently reduced to fit
`max_total_buffer_bytes` (256 MiB) across all active segments; the effective value is
reported back."
- `docs/04` §8: keep "≤ 60 MB RSS / 20 downloads" — it now holds at the 2 MiB default.
- ADR: record the throughput-plateau + global-cap-arithmetic reasoning; note 8 MiB was
considered for the ceiling and 16 MiB chosen for stall absorption.