Teach the covering search the no-reuse-src-regions rule instead of
throwing after plan selection:
- PlanSearchMatch carries the src identity (bytenr, or node address
with the top bit set for synthetic extents) and src interval.
- The exact DP refuses a dedupe edge whose src slice overlaps a slice
already consumed along the path on the same src key
(plan_src_edge_blocked counts refusals). The check walks the
state's back-trace, O(path) per tentative edge. Dominance pruning
still ignores src usage, so this is an admission heuristic, not an
exact src-aware search: a pruned state could in principle have
avoided a later conflict.
- The greedy fallback keeps a live set of absorbed matches and picks
the farthest-reaching one whose src slice is still free, instead of
the src-blind running argmax.
Because the search admits src-overlapping matches whose used slices
are disjoint, the whole-interval pre-compose check would false-fire;
the once-only trap moves into scan_next_compose_plan_tree, checking
the src slice of each emitted dedupe child (the exact bytes sent to
the kernel) and throwing on overlap.
This supersedes the whole-interval throw-and-skip added by
0913df4a45 "scan-next: enforce the src once-only rule at plan
composition" (removed here). A three-way comparison on
the same corpus showed the throw-and-skip variant discarding 640849
otherwise-buildable plans and freeing 28% less space at equal IO than
the pre-enforcement baseline, while enforcing inside the search freed
20% more space at equal IO than that baseline and finished the run
fastest of all variants (77.6M edges refused, only 259 residual
compose-trap throws from DP-slice vs compose-emission mismatch on
overlapping dst matches - a follow-up refinement).
Unit-test harness: make_inputs gives each synthetic match a distinct
src key with a src interval mirroring its dst interval, so existing
single-src-per-match scenarios are unaffected by the new check.
Assisted-by: Claude-Code:claude-fable-5
size_t copy_ops = 0;
size_t matched_blocks = 0;
+ // Once-only: src slices consumed by emitted dedupe segments, per
+ // src key, across the whole dst plan.
+ map<uint64_t, vector<pair<uint64_t, uint64_t>>> used_src;
+ const auto src_slice_free = [&](const PlanSearchMatch &m,
+ uint64_t seg_begin, uint64_t seg_end) -> bool {
+ const uint64_t sa = m.m_src_begin + (seg_begin - m.m_dst_begin);
+ const uint64_t sb = sa + (seg_end - seg_begin);
+ const auto it = used_src.find(m.m_src_key);
+ if (it == used_src.end()) return true;
+ for (const auto &iv : it->second) {
+ if (sa < iv.second && iv.first < sb) return false;
+ }
+ return true;
+ };
for (const auto ®ion : in.m_data_regions) {
uint64_t pos = region.m_begin;
size_t mp = 0; // sweep cursor into by_begin
- uint64_t best_end = region.m_begin; // running max clamped reach
- size_t best_idx = in.m_matches.size();
+ vector<size_t> live; // absorbed, dst_begin <= pos
while (pos < region.m_end) {
- // Absorb matches now eligible (dst_begin <= pos); keep the running
- // argmax of clamped dst_end. Monotone across the region -> O(M+V).
+ // Absorb matches now eligible (dst_begin <= pos) into the
+ // live set. Unlike the src-blind greedy's running argmax,
+ // the once-only check depends on pos, so the best live
+ // match is rescanned per step (O(M) worst case — this is
+ // the over-budget fallback path already).
while (mp < by_begin.size()
&& in.m_matches[by_begin[mp]].m_dst_begin <= pos) {
const size_t idx = by_begin[mp];
++mp;
if (m.m_dst_end <= region.m_begin
|| m.m_dst_begin >= region.m_end) continue;
+ live.push_back(idx);
+ }
+ // Pick the live match with the farthest clamped reach whose
+ // src slice for [pos, reach) is still free; equal reach
+ // breaks toward the higher-ref src (see the DP reach
+ // precompute).
+ uint64_t best_end = pos;
+ size_t best_idx = in.m_matches.size();
+ for (const size_t idx : live) {
+ const auto &m = in.m_matches[idx];
+ if (m.m_dst_end <= pos) continue;
const uint64_t end = min(m.m_dst_end, region.m_end);
- // Farther reach wins; equal reach breaks toward the
- // higher-ref src (see the DP reach precompute) so a
- // whole-extent cover consolidates onto the canonical
- // extent. Src choice does not change the edge's debt.
+ if (end <= pos) continue;
+ if (!src_slice_free(m, pos, end)) {
+ BEESCOUNT(plan_src_edge_blocked);
+ continue;
+ }
if (end > best_end
|| (end == best_end
&& best_idx != in.m_matches.size()
}
if (best_end > pos) {
// Dedupe edge [pos, best_end) served by match best_idx.
+ const auto &m = in.m_matches[best_idx];
+ const uint64_t sa = m.m_src_begin + (pos - m.m_dst_begin);
+ used_src[m.m_src_key].emplace_back(
+ sa, sa + (best_end - pos));
result.m_selected_matches.push_back(best_idx);
matched_blocks += block_count(pos, best_end);
++dedupe_ops;
size_t m_match_idx = 0; // when m_dedupe
uint64_t m_copy_begin = 0; // when !m_dedupe
uint64_t m_copy_end = 0;
+ // Once-only: the src slice this dedupe edge consumes.
+ uint64_t m_src_key = 0; // when m_dedupe
+ uint64_t m_src_begin = 0;
+ uint64_t m_src_end = 0;
};
// Persistent (immutable) singly-linked list, newest edge first.
// Appending an edge conses one node that shares the predecessor's
// check it once, before the inner loop.
if (cover_idx != in.m_matches.size()
&& within_limits(st.m_dedupe_ops + 1, st.m_copy_ops)) {
+ const auto &cm = in.m_matches[cover_idx];
for (size_t vj = vi + 1;
vj < nverts && verts[vj] <= reach; ++vj) {
const uint64_t b = verts[vj];
st.m_dedupe_ops + 1, st.m_copy_ops, nd)) {
continue;
}
+ // Once-only: refuse the edge if its src slice
+ // overlaps a slice already consumed along this
+ // path on the same src. O(path) per tentative
+ // edge; dominance pruning above keeps paths
+ // short. Pruning still ignores src usage, so
+ // this is an admission heuristic, not an exact
+ // src-aware search.
+ const uint64_t sa = cm.m_src_begin
+ + (a - cm.m_dst_begin);
+ const uint64_t sb = sa + (b - a);
+ bool src_reused = false;
+ for (const State::EdgeNode *n = st.m_edges.get();
+ n; n = n->m_prev.get()) {
+ const auto &e = n->m_edge;
+ if (e.m_dedupe
+ && e.m_src_key == cm.m_src_key
+ && sa < e.m_src_end
+ && e.m_src_begin < sb) {
+ src_reused = true;
+ break;
+ }
+ }
+ if (src_reused) {
+ BEESCOUNT(plan_src_edge_blocked);
+ continue;
+ }
State ns = st;
ns.m_dedupe_ops += 1;
ns.m_matched_blocks += blk;
ns.m_partial_debt = nd;
ns.m_edges = make_shared<const State::EdgeNode>(
State::EdgeNode{
- State::Edge{ true, cover_idx, 0, 0 },
+ State::Edge{ true, cover_idx, 0, 0,
+ cm.m_src_key, sa, sb },
st.m_edges });
add_state(at[vj], std::move(ns));
}
uint64_t m_dst_end = 0;
size_t m_group_id = 0; ///< caller-defined match identity
uint64_t m_src_ref_count = 0; ///< src refs()->size(), tie-break only
+ /// Src identity + interval for the once-only rule: the search
+ /// refuses a dedupe edge whose src slice overlaps a src slice
+ /// already used on the same src key along the path.
+ uint64_t m_src_key = 0;
+ uint64_t m_src_begin = 0;
+ uint64_t m_src_end = 0;
};
/// Fixed per-dst inputs to the search, mirroring scan_next_plan_init's
// src extents here were already resolved to produce these matches.
const uint64_t src_refs = rewrite_policy.m_prefer_canonical_src
? match.m_src.refs(layer)->size() : 0;
+ // Src identity for the search's once-only check: bytenr
+ // when the src has one; otherwise the node address with
+ // the top bit set, keeping the two keyspaces disjoint.
+ const auto src_bytenr = match.m_src.bytenr_opt();
+ const uint64_t src_key = src_bytenr ? *src_bytenr
+ : (reinterpret_cast<uintptr_t>(
+ match.m_src.extent_sp().get())
+ | (uint64_t(1) << 63));
in.m_matches.push_back(PlanSearchMatch{
- match.m_dst_begin, match.m_dst_end, flat.size(), src_refs });
+ match.m_dst_begin, match.m_dst_end, flat.size(), src_refs,
+ src_key, match.m_src_begin, match.m_src_end });
flat.push_back(FlatMatch{ &group, &match });
}
}
{
BeesSuperExtentBuilder builder;
size_t match_idx = 0;
+ // Once-only enforcement on what is actually emitted: each dedupe
+ // child's src slice, per src extent, must not overlap a slice this
+ // plan already emitted.
+ map<pair<uint64_t, const void *>, vector<pair<uint64_t, uint64_t>>> used_src;
const auto dst_bm = dst.full_block_map(&Borrower::current().layer());
for (const auto ®ion : dst_bm->m_regions) {
const auto sub_begin = m.m_src_begin + src_offset;
const auto src_sub = BeesExtent::subextent(m.m_src,
sub_begin, sub_begin + match_len);
+ const auto src_bytenr = m.m_src.bytenr_opt();
+ const auto src_key = src_bytenr
+ ? make_pair(*src_bytenr,
+ static_cast<const void *>(nullptr))
+ : make_pair(uint64_t(0),
+ static_cast<const void *>(
+ m.m_src.extent_sp().get()));
+ auto &ivals = used_src[src_key];
+ for (const auto &iv : ivals) {
+ if (sub_begin < iv.second
+ && iv.first < sub_begin + match_len) {
+ BEESCOUNT(plan_src_overlap);
+ BEESLOGERR("once-only rule violation at compose: dst "
+ << to_hex(dst.bytenr())
+ << " src " << m.m_src
+ << " slice [" << to_hex(sub_begin)
+ << ".." << to_hex(sub_begin + match_len)
+ << ") overlaps emitted ["
+ << to_hex(iv.first) << ".."
+ << to_hex(iv.second) << ")");
+ THROW_ERROR(runtime_error,
+ "once-only rule violation at compose: dst "
+ << to_hex(dst.bytenr()));
+ }
+ }
+ ivals.emplace_back(sub_begin, sub_begin + match_len);
const auto child = bees_make_dedupeextent(src_sub);
builder.append(BeesExtentSlice(child, 0, match_len));
pos = match_end;
PlanCost m_total_cost;
};
-/// Enforce the once-only rule at plan composition: each src block may
-/// be referenced at most once across the dedupe edges of a single dst
-/// plan. Overlapping src references within one plan stack multiple
-/// new refs onto the same physical blocks, which explodes reference
-/// counts on extremely common data; every observed violation has also
-/// been a planner bug pairing a dst with data the src never had
-/// (match transposition onto a non-identical src). Throws to reject
-/// the plan so it can never reach the kernel; callers log the
-/// exception and skip the extent.
-static void
-scan_next_enforce_src_once_only(
- const BeesExtent &dst,
- const vector<ExtentMatch> &matches)
-{
- BEESTRACE("scan_next_enforce_src_once_only dst " << to_hex(dst.bytenr()));
- // Key src extents by bytenr when they have one, node identity
- // otherwise (synthetic extents), then check the src intervals
- // under each key for pairwise overlap.
- map<pair<uint64_t, const void *>, vector<pair<uint64_t, uint64_t>>> src_ranges;
- for (const auto &m : matches) {
- const auto bytenr = m.m_src.bytenr_opt();
- const auto key = bytenr
- ? make_pair(*bytenr, static_cast<const void *>(nullptr))
- : make_pair(uint64_t(0),
- static_cast<const void *>(m.m_src.extent_sp().get()));
- src_ranges[key].emplace_back(m.m_src_begin, m.m_src_end);
- }
- for (auto &p : src_ranges) {
- auto &ranges = p.second;
- sort(ranges.begin(), ranges.end());
- for (size_t i = 1; i < ranges.size(); ++i) {
- if (ranges[i].first >= ranges[i - 1].second) {
- continue;
- }
- BEESCOUNT(plan_src_overlap);
- BEESLOGERR("once-only rule violation: dst "
- << to_hex(dst.bytenr()) << " plan references src blocks more than once");
- for (const auto &m : matches) {
- BEESLOGERR(" match dst [" << to_hex(m.m_dst_begin)
- << ".." << to_hex(m.m_dst_end)
- << ") src [" << to_hex(m.m_src_begin)
- << ".." << to_hex(m.m_src_end)
- << ") src extent " << m.m_src);
- }
- THROW_ERROR(runtime_error,
- "once-only rule violation: dst " << to_hex(dst.bytenr())
- << " src ranges [" << to_hex(ranges[i - 1].first)
- << ".." << to_hex(ranges[i - 1].second)
- << ") and [" << to_hex(ranges[i].first)
- << ".." << to_hex(ranges[i].second)
- << ") overlap");
- }
- }
-}
-
/// Try to build a DstPlan for one dst extent given candidate sources.
/// Returns nullopt if the plan is not viable (no matches, cost too
/// high, or choose_match_plan rejects).
[](const auto &lhs, const auto &rhs) {
return lhs.m_dst_begin < rhs.m_dst_begin;
});
- scan_next_enforce_src_once_only(dst, chosen->m_selected_matches);
-
+ // The once-only rule is enforced on the emitted dedupe slices inside
+ // scan_next_compose_plan_tree: the search admits src-overlapping
+ // *matches* so long as the *slices* a covering uses are disjoint, so
+ // a whole-interval check on the selected matches would false-fire
+ // here.
const auto plan_tree = scan_next_compose_plan_tree(
dst, chosen->m_selected_matches);
PlanSearchInputs in;
in.m_data_regions.push_back(PlanSearchRegion{ 0, n_blocks * BLOCK });
for (size_t i = 0; i < match_blocks.size(); ++i) {
+ // Each match models a distinct src extent (distinct src_key),
+ // with a src interval mirroring its dst interval, so the
+ // search's once-only src check never fires within these
+ // single-src-per-match scenarios.
in.m_matches.push_back(PlanSearchMatch{
match_blocks[i].first * BLOCK,
match_blocks[i].second * BLOCK,
- i });
+ i, 0,
+ /*src_key*/ 1000 + i,
+ match_blocks[i].first * BLOCK,
+ match_blocks[i].second * BLOCK });
}
in.m_dst_ref_count = ref_count;
in.m_dst_compressed = false;