return c;
}
+size_t
+scan_next_region_block_count(bool unreachable, uint64_t byte_count,
+ uint64_t block_size)
+{
+ // Unreachable rounds down; every other state rounds up. See the header
+ // for why only the unaligned-EOF tail is affected.
+ return static_cast<size_t>(unreachable
+ ? byte_count / block_size
+ : (byte_count + block_size - 1) / block_size);
+}
+
double
scan_next_covering_debt(const PlanSearchInputs &in,
const BeesRewritePolicy &policy,
bool dst_compressed, uint64_t dst_phys_size,
uint64_t good_blocks, uint64_t clone_alignment);
+/// Whole-block count of a block-map region's byte span for cost metrics.
+/// Rounds @p byte_count up to a whole block, except an unreachable region,
+/// which rounds down. The unaligned-EOF clip in block_map_fetch is the only
+/// source of a non-block-aligned region length: it ends the reachable
+/// (data / zero / prealloc) region mid-block at the file's EOF and inserts an
+/// unreachable region for the past-EOF remainder of that same physical block.
+/// The reachable side rounds up and claims the shared EOF block; the
+/// unreachable remainder rounds down to zero so it is not double-counted as a
+/// freeable block (rewriting it frees nothing — the copy re-materializes an
+/// identically-shaped block). Every other region is block-aligned, where
+/// floor and ceil coincide, so only the EOF tail is affected.
+size_t
+scan_next_region_block_count(bool unreachable, uint64_t byte_count,
+ uint64_t block_size);
+
/// Compute the signed space-debt (do-nothing-plan.md §4) of a covering with
/// the given operation/block counts. Thin wrapper: marshals the search's
/// inputs into scan_next_debt_fields() (the shared builder) and prices the
scan_next_plan_init(ChosenPlan &plan, const BeesExtent &dst)
{
const auto bm = dst.full_block_map(&Borrower::current().layer());
+ const auto bs = ranged_cast<uint64_t>(BLOCK_SIZE_SUMS);
for (const auto ®ion : bm->m_regions) {
const auto byte_count = region.m_end - region.m_begin;
- const auto block_count =
- (byte_count + ranged_cast<uint64_t>(BLOCK_SIZE_SUMS) - 1)
- / ranged_cast<uint64_t>(BLOCK_SIZE_SUMS);
+ // Whole-block cost accounting. scan_next_region_block_count
+ // rounds every state up except unreachable, so the unaligned-EOF
+ // tail (its only non-block-aligned input) does not claim a
+ // freeable block it shares with reachable data.
+ const auto block_count = scan_next_region_block_count(
+ region.m_state == BeesTreeBlockState::unreachable,
+ byte_count, bs);
switch (region.m_state) {
case BeesTreeBlockState::data:
plan.m_initial_data_blocks += block_count;
// Pure gain: no plan-tree op charged. The composer
// emits a HoleExtent for unreachable regions, but the
// rewrite cost model treats freeing them as costless.
+ // A sub-block past-EOF tail floors to zero: it shares its
+ // block with reachable data and frees nothing.
break;
default:
break;
size_t total_blocks = 0;
const auto start_bm = start.full_block_map(&Borrower::current().layer());
for (const auto ®ion : start_bm->m_regions) {
- const auto count = ranged_cast<size_t>(
- (region.m_end - region.m_begin + BLOCK_SIZE_SUMS - 1) / BLOCK_SIZE_SUMS);
+ // Whole-block cost accounting, matching scan_next_plan_init: an
+ // unreachable region rounds down so a sub-block past-EOF tail is
+ // not miscounted as a freeable block it shares with reachable
+ // data; every other region rounds up.
+ const auto count = scan_next_region_block_count(
+ region.m_state == BeesTreeBlockState::unreachable,
+ region.m_end - region.m_begin,
+ ranged_cast<uint64_t>(BLOCK_SIZE_SUMS));
total_blocks += count;
switch (region.m_state) {
case BeesTreeBlockState::zero:
}
}
+// The whole-block rounding rule for cost metrics, and the production
+// regression it fixes: an unaligned-EOF block that is entirely reachable data
+// must not manufacture a freeable "unreachable" block out of its past-EOF tail.
+static void
+test_region_block_count_eof_tail()
+{
+ const uint64_t bs = 4096;
+
+ // Block-aligned lengths: floor == ceil for every state, reachable or not.
+ assert(scan_next_region_block_count(false, 0, bs) == 0);
+ assert(scan_next_region_block_count(true, 0, bs) == 0);
+ assert(scan_next_region_block_count(false, bs, bs) == 1);
+ assert(scan_next_region_block_count(true, bs, bs) == 1);
+ assert(scan_next_region_block_count(false, 8 * bs, bs) == 8);
+ assert(scan_next_region_block_count(true, 8 * bs, bs) == 8);
+
+ // The unaligned-EOF split of one physical block: a reachable region of
+ // 2990 data bytes plus the inserted 1106-byte past-EOF unreachable tail.
+ // The reachable side rounds up and claims the block; the tail rounds down
+ // to zero. Together they count the one physical block exactly once.
+ const uint64_t data_bytes = 2990;
+ const uint64_t tail_bytes = bs - data_bytes; // 1106
+ assert(scan_next_region_block_count(false, data_bytes, bs) == 1);
+ assert(scan_next_region_block_count(true, tail_bytes, bs) == 0);
+
+ // A multi-block past-EOF tail still yields only its whole blocks: the
+ // shared EOF block belongs to the data side; the rest are real holes.
+ assert(scan_next_region_block_count(true, bs + tail_bytes, bs) == 1);
+
+ // End-to-end debt. Production ref-op-cost = 128, other costs default.
+ // The dst is one uncompressed 4K data block with a mid-block EOF, so the
+ // data block is copied (relocated) and the only candidate for good_blocks
+ // is the past-EOF tail. Pre-fix the tail counted as 1 unreachable block
+ // -> physical_freed = 4096 -> debt = 2*128 + 53 - 4096 = -3787 (a bogus
+ // win that also cleared free-min at 50%). Post-fix the tail is 0 blocks.
+ auto policy = default_policy();
+ policy.m_ref_op_cost = 128;
+ const uint64_t data_blocks = scan_next_region_block_count(false, data_bytes, bs);
+ const uint64_t good_blocks = scan_next_region_block_count(true, tail_bytes, bs);
+ assert(data_blocks == 1);
+ assert(good_blocks == 0);
+ const auto fixed = scan_next_debt_fields(
+ /*dst_ref_count=*/2, /*frag_count=*/1, /*copy_ops=*/1,
+ /*dst_compressed=*/false, /*dst_phys_size=*/0,
+ good_blocks, bs);
+ assert(fixed.m_physical_freed == 0);
+ assert(fixed.space_debt(policy) > 0.0); // do-nothing (0.0) now wins
+
+ // The pre-fix miscount reproduces the exact -3787 seen in production.
+ const auto buggy = scan_next_debt_fields(2, 1, 1, false, 0, /*good=*/1, bs);
+ assert(buggy.space_debt(policy) == -3787.0);
+}
+
int
main(int, char **)
{
+ RUN_A_TEST(test_region_block_count_eof_tail());
RUN_A_TEST(test_space_debt_worked_example());
RUN_A_TEST(test_space_debt_zero_cost_policy());
RUN_A_TEST(test_space_debt_no_overflow());