Options::memory_pages says it is "pages of anonymous memory the log may hold". Under Durability::Async it is not a bound on anything. It is a target the flusher works towards afterwards, and while the writing is going the log holds whatever the writer has appended.
Measured with a probe that samples resident_pages on a thread while a single session writes 8 KiB values, memory_pages: 2, mutable_pages: 1, compact_below: 0 so nothing but eviction moves the head:
| durability |
records |
written |
bound |
peak while writing |
| Async |
10000 |
80 MiB |
8 MiB |
48 MiB, 12 pages |
| Async |
40000 |
320 MiB |
8 MiB |
240 MiB, 60 pages |
| Durable |
40000 |
320 MiB |
8 MiB |
12 MiB, 3 pages |
Four times the load is five times the overshoot, so this is not a constant lag with a ceiling, it is the whole write staying in memory. At Durable the same load holds three pages, which is the bound plus the page being appended to and is exactly what the option promises.
The mechanism is not subtle once you look at it. Log::ensure_page allocates a page whenever an append reaches one, and it consults nothing:
let fresh = unsafe { alloc_zeroed(page_layout()) };
...
slot.store(fresh, Ordering::Release);
self.anonymous.fetch_add(1, Ordering::AcqRel);
The only thing that ever gives a page back is evict_behind, which runs from evict_settled, which is the flusher's. And evict_behind cannot take a page until it is durable:
if page_start(victim + 1) > self.flushed() {
break;
}
which is right, a page that left memory before its bytes were on the device would be a reader preading a hole. So at Durable the writer is its own back pressure, because it does not return until the bytes are down, and eviction always has something to take. At Async nothing connects the writer to the flusher at all, and the log grows in memory at whatever rate the CPU can append.
The existing test knows this and works around it. the_page_bound_holds_after_the_writing_stops in crates/zu2/tests/end_to_end.rs is named for it and asserts after drop(s) and db.sync(), with a comment that what is being waited for is a thread noticing. Nothing asserts anything about during.
Why this matters beyond the doc being wrong. #767 is heading towards making a bound the default, since the anonymous total at defaults is the whole log and memory_pages is usize::MAX. A bound that a bulk load walks straight through would give a default that looks right in the options and does nothing for the load phase of every benchmark we run, which is the phase where the memory figure is worst.
The fix is the one the log already uses for its other ceiling. A writer that fills the log waits for a compaction pass, a_writer_that_fills_the_log_waits_for_a_pass in db.rs covers it. The same shape applies here: an append that would take the anonymous count past memory_pages wakes the flusher and waits for it, rather than allocating and moving on. That turns the option into what it says, at the cost of turning a write burst that outruns the device into a wait, which is what a bound means.
Numbers above are from the laptop, and they are a behaviour observation rather than a throughput one: what is being claimed is that the peak scales with the load, which it does by a factor of five across a factor of four. Nothing published comes from this.
Options::memory_pagessays it is "pages of anonymous memory the log may hold". UnderDurability::Asyncit is not a bound on anything. It is a target the flusher works towards afterwards, and while the writing is going the log holds whatever the writer has appended.Measured with a probe that samples
resident_pageson a thread while a single session writes 8 KiB values,memory_pages: 2,mutable_pages: 1,compact_below: 0so nothing but eviction moves the head:Four times the load is five times the overshoot, so this is not a constant lag with a ceiling, it is the whole write staying in memory. At
Durablethe same load holds three pages, which is the bound plus the page being appended to and is exactly what the option promises.The mechanism is not subtle once you look at it.
Log::ensure_pageallocates a page whenever an append reaches one, and it consults nothing:The only thing that ever gives a page back is
evict_behind, which runs fromevict_settled, which is the flusher's. Andevict_behindcannot take a page until it is durable:which is right, a page that left memory before its bytes were on the device would be a reader preading a hole. So at
Durablethe writer is its own back pressure, because it does not return until the bytes are down, and eviction always has something to take. AtAsyncnothing connects the writer to the flusher at all, and the log grows in memory at whatever rate the CPU can append.The existing test knows this and works around it.
the_page_bound_holds_after_the_writing_stopsincrates/zu2/tests/end_to_end.rsis named for it and asserts afterdrop(s)anddb.sync(), with a comment that what is being waited for is a thread noticing. Nothing asserts anything about during.Why this matters beyond the doc being wrong. #767 is heading towards making a bound the default, since the anonymous total at defaults is the whole log and
memory_pagesisusize::MAX. A bound that a bulk load walks straight through would give a default that looks right in the options and does nothing for the load phase of every benchmark we run, which is the phase where the memory figure is worst.The fix is the one the log already uses for its other ceiling. A writer that fills the log waits for a compaction pass,
a_writer_that_fills_the_log_waits_for_a_passindb.rscovers it. The same shape applies here: an append that would take the anonymous count pastmemory_pageswakes the flusher and waits for it, rather than allocating and moving on. That turns the option into what it says, at the cost of turning a write burst that outruns the device into a wait, which is what a bound means.Options::memory_pageswhich durability modes it holds under, until it holds under bothNumbers above are from the laptop, and they are a behaviour observation rather than a throughput one: what is being claimed is that the peak scales with the load, which it does by a factor of five across a factor of four. Nothing published comes from this.