db/pager/tests: cleanup pass over the free list
No behaviour is meant to change and the gate confirms it: 1.94x / 679.2 MB
reclaimed / 9 rebuilds and 2.46x / 934.0 MB / 13 on the two 16 KiB lines,
0.0 MB on the 200-byte line, all identical to the numbers recorded for them.
Deduplication. `pages_for` was written in db.zig and again in pager.zig and
twice more inline; there is now one, public, and the two pre-existing copies
call it. `pages_per_map_align` replaces three hand-rolled `map_align /
page_size`. `SlabRun.window_first` was a stored field that could never legally
disagree with `first` and was maintained by hand at two sites -- now a method.
`keep_piece` re-derived `SlabRun.window_count` character for character; it
calls it. `insert_run` scanned linearly for a position `run_of` binary-searches
for, which made loading a fragmented catalog quadratic; both now go through one
`run_lower_bound`. Freeing a run's window map was written four times; one
helper. The 20% rebuild share was stated in `note_compact` and again in
`wants_rebuild`, with a comment arguing at length that they must be the same
number -- `worth_rewriting` makes that structural.
Efficiency. The identity assert in `reclaim_windows` called `dead_located()`,
an O(every window) walk, and `assert_msg` is live in ReleaseFast -- so it
doubled the scan the reclamation was about to make (2.75 MB streamed twice per
reclaiming checkpoint at the 21 GB the gate targets). `dead_located` is now a
maintained counter, the check is O(1) in every build, and the scan cross-checks
it while it is there. `SlabRun.full` lets a run with nothing to give be copied
without its counters being read at all, so the common case is O(runs) rather
than O(windows).
The pager's two allocation policies were hand-copying the claim step, and the
copy had already lost two of the three preconditions -- `alloc_slab_run` never
checked `pages <= reserved_pages`. Both now go through `claim_locked`.
`reclaimed_bytes` moves from Collection to Engine, beside `compactions`, which
is how it is read and the only place it can be honest: a life-of-the-process
total must not lose a dropped collection's share. Both join `Counters`, so
`slab_stats` stops opening `counter_lock` by hand.
The ownership assertion in `write_catalog` was gated on `is_test or Debug`, a
predicate nothing else in the codebase uses, which left the one silent failure
this design can produce unchecked in ReleaseSafe. It is now `!= ReleaseFast`,
the line `protect_stable` already draws. Measured: no change to the suite's
runtime.
Altitude. `note_checkpoint` was called from exactly one place, the tail of
`upsert` -- so a delete armed no checkpoint by any route, which is why
reclamation only ever ran when the *rebuild* trigger fired and the rebuild then
reset the window map it would have used. `remove` and the TTL sweep arm one
now, next to the `note_compact` calls that were added for the same omission a
milestone ago. `compact`'s leading checkpoint stays, demoted in its comment
from the mechanism to the local ordering it actually guarantees.
serverStatus reports `allocTailBytes`/`freeReadyBytes` instead of page counts,
so the harness stops hard-coding 4096 -- the kind of constant this milestone
was blindsided by once already.
tests/e2e/churn.js: `deleteMany({_id: {$in: [5000 ids]}})` exceeded
`index.max_combos`, so the planner refused the index and every delete became a
full collection scan re-filtering each document against 5000 members. That was
the entire runtime of the harness. One delete spec per id instead: the 40k x
16 KiB gate goes 57 s -> 6 s, and the 150k x 200 B line 483 s -> 3 s, with
identical output. Also: the per-round `countDocuments` is gone (the harness
knows the count), and the server log is a bounded ring rather than a rope that
grows with everything the server ever said.
Reverted from the review: reusing one MongoClient across the startup poll. A
client whose first connect fails tears its topology down and every later
command on it fails identically, so it turns "not up yet" into "never comes
up" -- it broke the first run. The reason is now a comment.
187/187 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz, e2e 49, e2e2
concurrent 2 and the crash pair, e2e3 16, e2e4 17, e2e6 72, e2e7 86,
crash-fuzz 60 cycles.
This commit was merged in pull request #1.
This commit is contained in:
@@ -108,6 +108,10 @@ const header_hashed_len: usize = 24;
|
||||
/// cursors up to this rather than to page_size.
|
||||
pub const map_align = std.heap.page_size_min;
|
||||
|
||||
/// Logical pages per system page. At least one: the comptime block below only
|
||||
/// requires one of the two sizes to divide the other.
|
||||
pub const pages_per_map_align: u32 = @max(1, map_align / page_size);
|
||||
|
||||
/// Growth granularity. Large enough that growth is rare and each `setLength`
|
||||
/// covers many allocations, and a multiple of every supported system page size.
|
||||
const grow_chunk_pages: u32 = 2048; // 8 MiB
|
||||
@@ -135,8 +139,9 @@ pub const Extent = struct {
|
||||
/// What a checkpoint publishes. Everything here is authoritative except the
|
||||
/// two cached counters, which are hints the engine recomputes if they look
|
||||
/// wrong.
|
||||
/// Pages a stream of `len` bytes occupies.
|
||||
fn pages_for(len: u64) u32 {
|
||||
/// Pages a run of `len` bytes occupies, rounded up. The one place the
|
||||
/// partial-page rule is written.
|
||||
pub fn pages_for(len: u64) u32 {
|
||||
return @intCast((len + page_size - 1) / page_size);
|
||||
}
|
||||
|
||||
@@ -700,7 +705,6 @@ pub const Pager = struct {
|
||||
} else {
|
||||
self.free_ready.items[i] = .{ .first = e.first + n, .pages = e.pages - n };
|
||||
}
|
||||
self.unprotect(first, n);
|
||||
return first;
|
||||
}
|
||||
|
||||
@@ -736,19 +740,19 @@ pub const Pager = struct {
|
||||
assert(min_pages <= max_pages);
|
||||
self.alloc_lock.lockUncancelable(self.io);
|
||||
defer self.alloc_lock.unlock(self.io);
|
||||
assert_msg(max_pages <= hold.pages, "a slab run request overran reserve_pages' promise");
|
||||
|
||||
// A split can leave a piece at each end, so one entry may become two.
|
||||
// Out of memory before anything is disturbed: the caller falls back to
|
||||
// bumping the tail, which is what it would have done anyway.
|
||||
self.free_ready.ensureUnusedCapacity(self.gpa, 1) catch return null;
|
||||
|
||||
const spp: u32 = if (map_align >= page_size) @intCast(map_align / page_size) else 1;
|
||||
var best: ?usize = null;
|
||||
var best_first: u32 = 0;
|
||||
var best_take: u32 = 0;
|
||||
var best_src: u32 = 0;
|
||||
for (self.free_ready.items, 0..) |e, i| {
|
||||
const from = std.mem.alignForward(u32, e.first, spp);
|
||||
const to = std.mem.alignBackward(u32, e.first + e.pages, spp);
|
||||
const from = std.mem.alignForward(u32, e.first, pages_per_map_align);
|
||||
const to = std.mem.alignBackward(u32, e.first + e.pages, pages_per_map_align);
|
||||
if (to <= from) continue;
|
||||
const usable = to - from;
|
||||
if (usable < min_pages) continue;
|
||||
@@ -756,16 +760,14 @@ pub const Pager = struct {
|
||||
// The longest run available, so the collection switches extents as
|
||||
// rarely as possible -- every switch abandons what is left of the
|
||||
// one before it. Ties go to the smallest source run, which leaves
|
||||
// the big ones as whole as it can.
|
||||
const better = if (best) |b|
|
||||
take > best_take or
|
||||
(take == best_take and e.pages < self.free_ready.items[b].pages)
|
||||
else
|
||||
true;
|
||||
if (better) {
|
||||
// the big ones as whole as it can. `best_take` starts at zero and
|
||||
// every candidate takes at least `min_pages`, so "nothing yet" is
|
||||
// already encoded.
|
||||
if (take > best_take or (take == best_take and e.pages < best_src)) {
|
||||
best = i;
|
||||
best_first = from;
|
||||
best_take = take;
|
||||
best_src = e.pages;
|
||||
}
|
||||
}
|
||||
const i = best orelse return null;
|
||||
@@ -781,10 +783,7 @@ pub const Pager = struct {
|
||||
} else {
|
||||
_ = self.free_ready.swapRemove(i);
|
||||
}
|
||||
self.reserved_pages -= best_take;
|
||||
hold.pages -= best_take;
|
||||
self.unprotect(best_first, best_take);
|
||||
self.mark_unpublished(best_first, best_take);
|
||||
self.claim_locked(hold, best_first, best_take);
|
||||
return .{ .first = best_first, .pages = best_take };
|
||||
}
|
||||
|
||||
@@ -823,33 +822,38 @@ pub const Pager = struct {
|
||||
/// For callers already holding `alloc_lock`; see `reserve_pages_locked`.
|
||||
fn alloc_assume_reserved_locked(self: *Pager, hold: *Reservation, n: u32) u32 {
|
||||
assert(n > 0);
|
||||
assert_msg(
|
||||
n <= hold.pages,
|
||||
"page allocation overran reserve_pages' promise",
|
||||
);
|
||||
assert_msg(
|
||||
n <= self.reserved_pages,
|
||||
"page allocation overran the pager's total promise",
|
||||
);
|
||||
assert_msg(
|
||||
self.alloc_tail + n <= self.mapped_pages,
|
||||
"page allocation past the mapped end of the data file",
|
||||
);
|
||||
self.reserved_pages -= n;
|
||||
hold.pages -= n;
|
||||
// Reuse before growing. Without this the free list is decorative and the
|
||||
// file grows without bound under churn, because copy-on-write abandons
|
||||
// every page it touches in every generation (PLAN amendment A2).
|
||||
if (self.take_free(n)) |recycled| {
|
||||
self.mark_unpublished(recycled, n);
|
||||
self.claim_locked(hold, recycled, n);
|
||||
return recycled;
|
||||
}
|
||||
const first = self.alloc_tail;
|
||||
self.alloc_tail += n;
|
||||
self.mark_unpublished(first, n);
|
||||
self.claim_locked(hold, first, n);
|
||||
return first;
|
||||
}
|
||||
|
||||
/// Charge `[first, first+pages)` against the reservation and make it
|
||||
/// writable. The one place a claim is booked, because there are two
|
||||
/// allocation policies above it and hand-copying this is how they drift --
|
||||
/// the copy in `alloc_slab_run` had already lost one of the preconditions.
|
||||
fn claim_locked(self: *Pager, hold: *Reservation, first: u32, pages: u32) void {
|
||||
assert_msg(pages <= hold.pages, "page allocation overran reserve_pages' promise");
|
||||
assert_msg(pages <= self.reserved_pages, "page allocation overran the pager's total promise");
|
||||
self.reserved_pages -= pages;
|
||||
hold.pages -= pages;
|
||||
// A recycled page was inside a published image once, so its protection
|
||||
// has to be lifted before it is handed out again.
|
||||
self.unprotect(first, pages);
|
||||
self.mark_unpublished(first, pages);
|
||||
}
|
||||
|
||||
fn mark_unpublished(self: *Pager, first: u32, n: u32) void {
|
||||
// `grow_to` sizes the set to the mapping, and `reserve_pages` has already
|
||||
// grown the mapping past this run, so the range is in bounds.
|
||||
@@ -963,7 +967,7 @@ pub const Pager = struct {
|
||||
// where the chunk stops being `grow_chunk_pages`.
|
||||
const chunk = @max(grow_chunk_pages, self.mapped_pages / 8);
|
||||
var new_pages = std.mem.alignForwardAnyAlign(u32, want_pages, chunk);
|
||||
const sys_pages: u32 = @intCast(map_align / page_size);
|
||||
const sys_pages: u32 = pages_per_map_align;
|
||||
if (sys_pages > 1) new_pages = std.mem.alignForward(u32, new_pages, sys_pages);
|
||||
|
||||
if (@as(u64, new_pages) << page_shift > self.reserve.len) {
|
||||
@@ -1271,7 +1275,7 @@ pub const Pager = struct {
|
||||
self.alloc_lock.lockUncancelable(self.io);
|
||||
defer self.alloc_lock.unlock(self.io);
|
||||
const bound = self.free_ready.items.len + self.free_hold.items.len + self.free_pending.items.len;
|
||||
const pages: u32 = @intCast((8 + bound * 8 + 8 + page_size - 1) / page_size);
|
||||
const pages: u32 = pages_for(8 + bound * 8 + 8);
|
||||
var hold: Reservation = .{};
|
||||
try self.reserve_pages_locked(&hold, pages);
|
||||
const first = self.alloc_assume_reserved_locked(&hold, pages);
|
||||
@@ -2097,7 +2101,7 @@ test "a slab run comes off the free list aligned, or not at all" {
|
||||
var tp = try TmpPager.init(io, 64 << 20);
|
||||
defer tp.deinit();
|
||||
const pg = tp.pg();
|
||||
const spp: u32 = @intCast(map_align / page_size);
|
||||
const spp: u32 = pages_per_map_align;
|
||||
|
||||
// A long run deliberately starting one 4 KiB page past a boundary, and a
|
||||
// short one, kept apart so coalescing cannot merge them.
|
||||
|
||||
Reference in New Issue
Block a user