Charge the snapshot-sharing undercharge in the do-nothing cost model.
refs_added credits one removed item per rewritten ref, but when k refs
through k subvols of one snapshot family are a single shared metadata
item, replacing the group writes k*F private items and removes only
the one shared item (freed by btrfs once the last sharer replaces its
ref) — the per-logical-ref accounting is short by exactly k-1 per
group, independent of F. Without the charge the planner performs
dedupes in snapshotted subvols whose physical win is smaller than the
metadata pages they unshare.
New CostReport::m_snap_extra carries the per-dst sum of (k-1) over
(cluster, inode, offset) ref groups, priced in space_debt() as
snap_extra * ref-cost * rewrite.snapshot-sharing. The term is a
per-dst constant (no F or op-mix dependence), so the state-collapse
invariant holds and the covering search is unchanged; it also makes
orientation snapshot-aware — a snapshot-family extent is cheaper as
src than as dst. Both debt paths marshal it through the shared
scan_next_debt_fields builder; the Plan B per-dst do-nothing gate and
aggregate pick it up, and the prefer-canonical-src pivot swaps the
start's and center's contributions along with their roles.
rewrite.snapshot-sharing is the expected ratio of subvol metadata
sharing: 1.0 (default) = undiverged snapshots, exactly cancelling the
invalid removal credit; 0 disables the term and the cluster-map work;
outside [0,1] scales for unusually expensive or cheap metadata. Refs
of snapshots that diverged past matching vanish from the dst ref list,
so they stop being charged with no special handling.
The plan: line grows a trailing 'snap <N>' field (appended, so
analyze-bees-log.sh parsing is unaffected), documented in running.md.
Unit tests pin the worked example at sharing 1.0 / 0 / 10, the
disabled path, and shared-builder carriage of snap_extra.
Assisted-by: Claude-Code:claude-fable-5
unchanged until an operator opts in).
* Accepts a [size value](config-format.md#size-values).
+* **`snapshot-sharing`**
+ Expected ratio of subvol metadata sharing between snapshots, weighting the
+ extra metadata cost of deduping in snapshotted subvols. Snapshot creation
+ shares the source subvol's metadata pages with the snapshot (bees detects
+ the relationship through each subvol's `uuid` and `parent_uuid`, including
+ snapshots whose common parent has since been deleted). While the sharing
+ lasts, `k` references to the same file range seen through `k` subvols of one
+ snapshot family are a *single* shared metadata item, and redirecting them
+ makes each subvol copy its metadata pages: `k` new items are written while
+ only the one shared item is eventually removed (btrfs frees the shared page
+ once the last subvol has replaced its reference). The per-reference
+ accounting of `ref-cost` is short by exactly `k − 1` items per shared group;
+ this option scales that correction, adding
+ `(k − 1) × ref-cost × snapshot-sharing` per group to the plan's debt.
+ References in subvols with no snapshot relationship are unaffected, and a
+ snapshot whose data has diverged past matching no longer appears among the
+ destination's references, so it stops being charged automatically.
+ * `1.0` assumes snapshots have not diverged — every shared group still pays
+ the full correction. `0` disables the term (and the snapshot detection
+ work) entirely. Values below `1.0` model partially-diverged snapshots;
+ values above `1.0` additionally price expensive metadata — rotating
+ storage, or allocation-preferences setups with dedicated fixed-size
+ metadata devices, where an unshared 16K metadata page costs far more than
+ the items written into it.
+ * Default: `1.0`.
+ * Accepts a [ratio value](config-format.md#ratio-values) (e.g. `1.0`, `50%`).
+
* **`prefer-canonical-src`**
When several source extents match a destination equally well — they cover the
same span, so the choice between them does not change the plan's cost — prefer
Each destination that is rewritten also prints a `plan:` line describing the
chosen plan and why it was chosen:
- plan: 128K 1d0c0p0u 7.787s/8.600s {0x2fce257000} [Dddddd] refs 9912 rc 10041 debt -7197.0
+ plan: 128K 1d0c0p0u 7.787s/8.600s {0x2fce257000} [Dddddd] refs 9912 rc 10041 debt -7197.0 snap 0
* `128K` — the destination's logical size.
* `1d0c0p0u` — operation counts for this destination: `d` dedupe, `c` copy
do-nothing is exactly `0`. A destination with a large `refs` and a negative
`debt` was worth rewriting because the space it frees outweighs the cost of
moving its references.
+ * `snap <N>` — the snapshot-sharing undercharge the winning plan's debt
+ carried: the number of extra metadata items unshared by rewriting
+ references in snapshotted subvols (what `rewrite.snapshot-sharing`
+ prices). `0` when the term is disabled or no destination reference
+ shares metadata pages.
* `pivot` — present when this destination belongs to a re-centered plan: with
`rewrite.prefer-canonical-src`, the dedupe star was pivoted onto a
higher-reference whole-extent duplicate, so this destination (possibly the
# Size value in bytes; 0 disables. See docs/config-file.md.
ref-op-cost = 0
+ # Expected ratio of subvol metadata sharing between snapshots.
+ # Weights the extra ref-cost charged when dedupe unshares snapshot
+ # metadata pages. Decimal ratio; 1.0 = undiverged snapshots,
+ # 0 disables. See docs/config-file.md.
+ snapshot-sharing = 1.0
+
# Prefer the most-referenced source on equal-length dedupe matches, to
# consolidate duplicates onto a canonical extent. Boolean; only safe
# with a non-zero ref-op-cost. See docs/config-file.md.
rv.ref_cost = load("rewrite.ref-cost", bees_parse_size);
rv.extent_cost = load("rewrite.extent-cost", bees_parse_size);
rv.m_ref_op_cost = load("rewrite.ref-op-cost", bees_parse_size);
+ rv.m_snapshot_sharing = load("rewrite.snapshot-sharing", bees_parse_ratio);
rv.m_plan_max_vertices = load("rewrite.plan-max-vertices", bees_parse_count);
rv.m_restart_max = load("rewrite.restart-max", bees_parse_count);
rv.m_insert_sample_max = load("rewrite.insert-sample-max", bees_parse_count);
double
CostReport::space_debt(const BeesRewritePolicy &policy) const
{
- // do-nothing-plan.md §4:
+ // do-nothing-plan.md §4 plus the snapshot-sharing undercharge
+ // (snapshot-metadata-cost.md §3):
// debt = refs_added * ref_cost
// + refs_created * ref_op_cost
// + extents_delta * extent_cost
// - physical_freed
+ // + snap_extra * ref_cost * snapshot_sharing
// Lower is better; negative is a net win; do-nothing is 0. Computed
// in double so refs_added * ref_cost cannot overflow int64 before the
// physical credit is subtracted; the result is only an ordering key,
// so precision loss past 2^53 does not matter. ref_op_cost defaults to
- // 0 (the term vanishes) unless an operator opts in.
+ // 0 (the term vanishes) unless an operator opts in. snapshot_sharing
+ // scales the per-group k-1 correction: at the default 1.0 it exactly
+ // cancels the removal credit refs_added wrongly grants shared refs,
+ // and 0 turns the term off (callers then skip computing snap_extra).
return static_cast<double>(m_refs_added) * static_cast<double>(policy.ref_cost)
+ static_cast<double>(m_refs_created) * static_cast<double>(policy.m_ref_op_cost)
+ static_cast<double>(m_extents_delta) * static_cast<double>(policy.extent_cost)
- - static_cast<double>(m_physical_freed);
+ - static_cast<double>(m_physical_freed)
+ + static_cast<double>(m_snap_extra) * static_cast<double>(policy.ref_cost)
+ * policy.m_snapshot_sharing;
}
ostream &
<< ", refs_created = " << c.m_refs_created
<< ", extents_delta = " << c.m_extents_delta
<< ", physical_freed = " << pretty(c.m_physical_freed)
+ << ", snap_extra = " << c.m_snap_extra
<< ", space_debt = " << c.m_space_debt
<< " }";
}
CostReport
scan_next_debt_fields(uint64_t dst_ref_count, size_t frag_count, size_t copy_ops,
bool dst_compressed, uint64_t dst_phys_size,
- uint64_t good_blocks, uint64_t clone_alignment)
+ uint64_t good_blocks, uint64_t clone_alignment,
+ uint64_t snap_extra)
{
CostReport c;
+ // snap_extra: precomputed per-dst snapshot-sharing undercharge,
+ // carried through so both debt paths price it identically.
+ c.m_snap_extra = snap_extra;
+
// refs_added = dst_ref_count * (F - 1); guard F <= 1 so it can't wrap.
if (frag_count > 1) {
c.m_refs_added = dst_ref_count
+ in.m_unreachable_blocks + matched_blocks;
const auto c = scan_next_debt_fields(in.m_dst_ref_count, frag_count,
copy_ops, in.m_dst_compressed, in.m_dst_phys_size,
- good_blocks, in.m_clone_alignment);
+ good_blocks, in.m_clone_alignment, in.m_snap_extra);
return c.space_debt(policy);
}
/// Physical bytes the plan recovers: phys_size for a compressed dst
/// removed wholesale, (blocks removed * blocksize) for uncompressed.
uint64_t m_physical_freed = 0;
+ /// Snapshot-sharing metadata undercharge (snapshot-metadata-cost.md
+ /// §3): sum of (k - 1) over groups of dst refs that are one physical
+ /// shared item seen through k subvols of one snapshot family. Those
+ /// groups cost k*F private items minus one shared removal, while
+ /// refs_added charges k*(F-1) — short by k-1 regardless of F.
+ /// Weighted by ref_cost * snapshot-sharing in space_debt(); 0 for
+ /// refs outside any family, where refs_added is already correct.
+ uint64_t m_snap_extra = 0;
/// Finalized signed space debt (do-nothing-plan.md §4), in bytes:
/// refs_added * ref_cost + refs_created * ref_op_cost
/// + extents_delta * extent_cost - physical_freed
+ /// + snap_extra * ref_cost * snapshot_sharing
/// Lower is better; negative is a net win; do-nothing is exactly 0.0.
/// Computed in double so refs_added * ref_cost cannot overflow before
/// the physical credit is subtracted; the value is only an ordering
uint64_t m_dst_ref_count = 0; ///< dst.refs()->size()
bool m_dst_compressed = false; ///< compressed dst: no copy edges unless m_force_rewrite
uint64_t m_dst_phys_size = 0; ///< whole-extent physical credit when compressed
+ uint64_t m_snap_extra = 0; ///< snapshot-sharing undercharge, per-dst constant
/// Force-rewrite (filter force-rewrite = yes) lifts the compressed-dst
/// copy prohibition: the operator has scoped, per filter, exactly which
/// extents_delta = +1 if any copy (one shared tempfile extent), else -1 (dst
/// fully freed). copy_ops > 0 iff unmatched data remains, so
/// this single test matches the old bad_blocks==0 form.
+/// snap_extra = passed through: the dst's snapshot-sharing undercharge
+/// (scan_next_snap_extra of the refs' family groups), a
+/// per-dst constant with no F dependence.
/// @p good_blocks is the already-summed matched + zero-like + unreachable count.
CostReport
scan_next_debt_fields(uint64_t dst_ref_count, size_t frag_count, size_t copy_ops,
bool dst_compressed, uint64_t dst_phys_size,
- uint64_t good_blocks, uint64_t clone_alignment);
+ uint64_t good_blocks, uint64_t clone_alignment,
+ uint64_t snap_extra);
/// One subvol's identity for snapshot-family clustering
/// (snapshot-metadata-cost.md §2): the root id plus the uuid and
return oss.str();
}
+// Defined below scan_next_dst_debt; used by the anon-namespace flatten
+// path (scan_next_choose_match_plan) as well.
+static uint64_t scan_next_snap_extra_refs(BeesExtentLayer &layer,
+ const BeesRewritePolicy &policy, const BeesExtent &dst);
+
namespace {
// ExtentMatch and its list typedef live in bees-plan.h so they can be
in.m_dst_ref_count = dst.refs(layer)->size();
in.m_dst_compressed = dst.compress_type(layer) != BTRFS_COMPRESS_NONE;
in.m_dst_phys_size = dst.phys_size();
+ in.m_snap_extra = scan_next_snap_extra_refs(layer,
+ rewrite_policy, dst);
in.m_force_rewrite = force_rewrite;
in.m_sums_block_size = ranged_cast<uint64_t>(BLOCK_SIZE_SUMS);
in.m_clone_alignment = ranged_cast<uint64_t>(BeesContext::s_clone_alignment);
/// m_dst_ref_count (read fresh at build): if this is ~0 while that is large,
/// the planner priced the dst against a stale/reduced ref view.
uint64_t m_sel_refs_created = 0;
+ /// Snapshot-sharing undercharge the winning plan's cost() carried
+ /// (CostReport::m_snap_extra — the sum of (k-1) over snapshot-family
+ /// ref groups). Logged as "snap" on the "plan:" line so dedupes
+ /// paying the snapshot term are greppable; 0 when the term is off or
+ /// no dst ref shares metadata pages.
+ uint64_t m_sel_snap_extra = 0;
/// True when this dst belongs to a re-centered Plan B (prefer-canonical-src
/// pivoted the star onto a higher-ref whole-extent duplicate, so this dst —
/// possibly the former start — is deduped onto that canonical). Logged as a
/// debt) than the unsplit edge, so the debt-minimizing selector never
/// places it on a chosen plan. rewrite_ops() therefore matches what
/// btrfs materializes wherever the debt is consulted.
+/// Compute the dst's snapshot-sharing undercharge (snapshot-metadata-cost.md
+/// §3): group the refs by (snapshot-family cluster, inode, offset) and sum
+/// (k - 1) over the groups — refs that are one physical metadata item seen
+/// through k subvols. 0 when the term is disabled (snapshot-sharing <= 0,
+/// which also skips the cluster-map build) or no ref belongs to a
+/// multi-member family. refs() and snap_cluster() are layer-cached, so
+/// repeated pricing of one dst within a transid cycle costs no ioctls.
+static uint64_t
+scan_next_snap_extra_refs(BeesExtentLayer &layer,
+ const BeesRewritePolicy &policy, const BeesExtent &dst)
+{
+ if (!(policy.m_snapshot_sharing > 0.0)) {
+ return 0;
+ }
+ const auto refs_sp = dst.refs(layer);
+ vector<array<uint64_t, 3>> keys;
+ for (const auto &ref : *refs_sp) {
+ const auto cluster = layer.snap_cluster(ref.root());
+ if (cluster == 0) {
+ continue;
+ }
+ keys.push_back({ cluster, ref.inum(), ref.offset() });
+ }
+ const auto rv = scan_next_snap_extra(std::move(keys));
+ if (rv > 0) {
+ BEESCOUNT(plan_snap_extra);
+ }
+ return rv;
+}
+
static CostReport
scan_next_dst_debt(BeesExtentLayer &layer,
+ const BeesRewritePolicy &policy,
const BeesExtent &dst, const ChosenPlan &cp)
{
// Marshal this dst's ChosenPlan into the shared builder:
dst_compressed,
dst.phys_size(),
cp.good_blocks(),
- static_cast<uint64_t>(BeesContext::s_clone_alignment));
+ static_cast<uint64_t>(BeesContext::s_clone_alignment),
+ scan_next_snap_extra_refs(layer, policy, dst));
}
// -----------------------------------------------------------------------------
// orientation, so no separate compress-bias tiebreaker is computed.
{
const auto debt = scan_next_dst_debt(
- Borrower::current().layer(), m_start, cp);
+ Borrower::current().layer(), m_policy, m_start, cp);
m_cost.m_refs_added = debt.m_refs_added;
m_cost.m_refs_created = debt.m_refs_created;
m_cost.m_extents_delta = debt.m_extents_delta;
m_cost.m_physical_freed = debt.m_physical_freed;
+ m_cost.m_snap_extra = debt.m_snap_extra;
}
m_cost.m_space_debt = m_cost.space_debt(m_policy);
// the plan's signed debt is finalized.
{
const auto debt = scan_next_dst_debt(
- Borrower::current().layer(), candidate, *chosen);
+ Borrower::current().layer(), m_policy, candidate, *chosen);
entry.m_entry_cost.m_refs_added = debt.m_refs_added;
entry.m_entry_cost.m_refs_created = debt.m_refs_created;
entry.m_entry_cost.m_extents_delta = debt.m_extents_delta;
entry.m_entry_cost.m_physical_freed = debt.m_physical_freed;
+ entry.m_entry_cost.m_snap_extra = debt.m_snap_extra;
}
// Per-dst benefit gate. Plan B executes each dst independently (each is a
m_cost.m_refs_created += e.m_entry_cost.m_refs_created;
m_cost.m_extents_delta += e.m_entry_cost.m_extents_delta;
m_cost.m_physical_freed += e.m_entry_cost.m_physical_freed;
+ m_cost.m_snap_extra += e.m_entry_cost.m_snap_extra;
}
// Finalize the aggregate signed debt (do-nothing-plan.md §4) where
// physical space and the extent delta is unchanged; only the moved-
// reference count changes, dropping by (center_refs - start_refs) — the
// churn the pivot saves. Adjust the aggregate debt and re-price.
+ // The snapshot-sharing undercharge swaps with the roles too: the
+ // center's refs are no longer modified (its groups drop out) and the
+ // start's refs now are (its groups are charged).
m_cost.m_refs_created = m_cost.m_refs_created - center_refs + start_refs;
+ m_cost.m_snap_extra = m_cost.m_snap_extra
+ - center->m_entry_cost.m_snap_extra
+ + scan_next_snap_extra_refs(layer, m_policy, m_start);
m_cost.m_space_debt = m_cost.space_debt(m_policy);
}
<< bar << "] refs " << dst_plan.m_dst_ref_count
<< " rc " << dst_plan.m_sel_refs_created
<< " debt " << setprecision(1) << dst_plan.m_space_debt
+ << " snap " << dst_plan.m_sel_snap_extra
<< (dst_plan.m_pivoted ? " pivot" : ""));
return true;
const auto winner_cost = winner.cost();
const double winner_space_debt = winner_cost.m_space_debt;
const uint64_t winner_refs_created = winner_cost.m_refs_created;
+ const uint64_t winner_snap_extra = winner_cost.m_snap_extra;
if (auto *pa = dynamic_cast<const BeesStartAsDstPlan *>(&winner)) {
const auto *impl = pa->impl();
dp->m_space_debt = winner_space_debt;
dp->m_sel_refs_created = winner_refs_created;
+ dp->m_sel_snap_extra = winner_snap_extra;
dp->m_dst_ref_count = start.refs(diag_layer)->size();
out.m_total_cost = dp->m_cost;
for (const auto &g : groups) {
if (dp) {
dp->m_space_debt = winner_space_debt;
dp->m_sel_refs_created = winner_refs_created;
+ dp->m_sel_snap_extra = winner_snap_extra;
dp->m_dst_ref_count = dst.refs(diag_layer)->size();
dp->m_pivoted = true;
out.m_total_cost += dp->m_cost;
if (dp) {
dp->m_space_debt = winner_space_debt;
dp->m_sel_refs_created = winner_refs_created;
+ dp->m_sel_snap_extra = winner_snap_extra;
dp->m_dst_ref_count =
entry.m_candidate.refs(diag_layer)->size();
out.m_total_cost += dp->m_cost;
/// reference cost. Authoritative default lives in bees-config-v2.cc
/// (0 = disabled); the C++ initializer is an invalid sentinel.
uint64_t m_ref_op_cost = UINT64_MAX;
+ /// Expected ratio of subvol metadata sharing between snapshots
+ /// (snapshot-metadata-cost.md). Weights the snapshot-sharing
+ /// undercharge: a group of k dst refs that is one physical metadata
+ /// item seen through k subvols of a snapshot family costs k*F
+ /// private items minus one shared removal when replaced, while the
+ /// per-logical-ref accounting charges k*(F-1) — short by k-1. Each
+ /// group adds (k-1) * ref_cost * this ratio to the debt. 1.0 =
+ /// snapshots never diverged (full charge); 0 = fully diverged (term
+ /// off, cluster lookups skipped); outside [0,1] scales for unusually
+ /// expensive or cheap metadata. Authoritative default lives in
+ /// bees-config-v2.cc; the C++ initializer (negative) is an invalid
+ /// sentinel.
+ double m_snapshot_sharing = -1.0;
/// When several source extents match a destination equally well (they cover
/// the same span), prefer the source that already has the most references,
/// so duplicates consolidate onto a canonical extent instead of an
p.ref_cost = 53;
p.extent_cost = 53;
p.m_ref_op_cost = 0;
+ p.m_snapshot_sharing = 1.0;
p.m_plan_max_vertices = 16384;
p.m_read_size = 1024 * 1024;
return p;
const auto c = scan_next_debt_fields(
/*dst_ref_count=*/3000, /*frag_count=*/1, /*copy_ops=*/0,
/*dst_compressed=*/true, /*dst_phys_size=*/131072,
- /*good_blocks=*/32, /*clone_alignment=*/4096);
+ /*good_blocks=*/32, /*clone_alignment=*/4096,
+ /*snap_extra=*/0);
assert(c.m_refs_added == 0); // F=1 -> no metadata growth
assert(c.m_refs_created == 3000); // work still priced
assert(c.m_physical_freed == 131072); // compressed: whole phys_size
const auto c = scan_next_debt_fields(
/*dst_ref_count=*/10, /*frag_count=*/3, /*copy_ops=*/1,
/*dst_compressed=*/false, /*dst_phys_size=*/0,
- /*good_blocks=*/5, /*clone_alignment=*/4096);
+ /*good_blocks=*/5, /*clone_alignment=*/4096,
+ /*snap_extra=*/0);
assert(c.m_refs_added == 10 * (3 - 1)); // dst_ref_count*(F-1)
assert(c.m_refs_created == 10 * 3); // dst_ref_count*F
assert(c.m_physical_freed == 5 * 4096); // uncompressed: good_blocks*bs
}
// F=0 edge (no ops): refs_added guarded against wrap, refs_created 0.
{
- const auto c = scan_next_debt_fields(999, 0, 0, false, 0, 0, 4096);
+ const auto c = scan_next_debt_fields(999, 0, 0, false, 0, 0, 4096, 0);
assert(c.m_refs_added == 0);
assert(c.m_refs_created == 0);
}
const auto in = make_inputs(1, { {0, 1} }, /*ref=*/3000);
const auto c = scan_next_debt_fields(3000, /*F=*/1, /*copy=*/0,
/*compressed=*/false, /*phys=*/0, /*good_blocks=*/1,
- /*clone_alignment=*/BLOCK);
+ /*clone_alignment=*/BLOCK, /*snap_extra=*/0);
+ assert(c.space_debt(policy)
+ == scan_next_covering_debt(in, policy, 1, 0, 1));
+ }
+ // snap_extra is carried through the shared builder and priced by both
+ // paths identically (the ref-op-cost inertness lesson): a search input
+ // with m_snap_extra set must reconcile with the builder's CostReport.
+ {
+ auto policy = default_policy();
+ auto in = make_inputs(1, { {0, 1} }, /*ref=*/4);
+ in.m_snap_extra = 3;
+ const auto c = scan_next_debt_fields(4, /*F=*/1, /*copy=*/0,
+ /*compressed=*/false, /*phys=*/0, /*good_blocks=*/1,
+ /*clone_alignment=*/BLOCK, /*snap_extra=*/3);
+ assert(c.m_snap_extra == 3);
assert(c.space_debt(policy)
== scan_next_covering_debt(in, policy, 1, 0, 1));
}
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);
+ good_blocks, bs, /*snap_extra=*/0);
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);
+ const auto buggy = scan_next_debt_fields(2, 1, 1, false, 0, /*good=*/1, bs, 0);
assert(buggy.space_debt(policy) == -3787.0);
}
}) == 2);
}
+static void
+test_snapshot_sharing_term()
+{
+ // The design's worked example: whole-extent dedupe (F=1), compressed
+ // 128K->4K dst, 10 refs in one k=10 family group (snap_extra = 9).
+ // base debt = -1*53 - 4096 = -4149; snap term = 9 * 53 * sharing.
+ const auto fields = [](uint64_t snap_extra) {
+ return scan_next_debt_fields(
+ /*dst_ref_count=*/10, /*frag_count=*/1, /*copy_ops=*/0,
+ /*dst_compressed=*/true, /*dst_phys_size=*/4096,
+ /*good_blocks=*/32, /*clone_alignment=*/4096,
+ snap_extra);
+ };
+
+ // Default sharing 1.0: -4149 + 477 = -3672, still a win.
+ {
+ const auto policy = default_policy();
+ assert(fields(9).space_debt(policy) == -3672.0);
+ }
+
+ // sharing = 0 turns the term off: back to the base debt even with
+ // snap_extra set.
+ {
+ auto policy = default_policy();
+ policy.m_snapshot_sharing = 0.0;
+ assert(fields(9).space_debt(policy) == -4149.0);
+ }
+
+ // sharing = 10 (expensive metadata): -4149 + 4770 = +621 — the
+ // do-nothing floor (0.0) wins.
+ {
+ auto policy = default_policy();
+ policy.m_snapshot_sharing = 10.0;
+ const auto debt = fields(9).space_debt(policy);
+ assert(debt == 621.0);
+ assert(!(debt < 0.0));
+ }
+
+ // No family refs: the term contributes nothing at any sharing value.
+ {
+ auto policy = default_policy();
+ policy.m_snapshot_sharing = 10.0;
+ assert(fields(0).space_debt(policy) == -4149.0);
+ }
+}
+
int
main(int, char **)
{
RUN_A_TEST(test_search_prefers_high_ref_src());
RUN_A_TEST(test_snap_cluster_map());
RUN_A_TEST(test_snap_extra_grouping());
+ RUN_A_TEST(test_snapshot_sharing_term());
return 0;
}