#include "bees-extent-tree.h"
#include "bees-extent.h"
#include "bees-fdcache.h"
+#include "bees-plan.h" // scan_next_snap_cluster_map
#include "bees.h"
#include "crucible/btrfs-tree.h"
<< ", ref cache with " << ref_cache_drop.size() << " inodes, " << ref_count << " refs");
BEESCOUNT(layer_extent_clear);
BEESCOUNT(layer_ref_clear);
+
+ // Snapshot families change only at a commit, so the cluster map is
+ // exact for a whole transid cycle; drop it with the other caches.
+ {
+ unique_lock<mutex> snap_lock(m_snap_cluster_mutex);
+ m_snap_clusters_built = false;
+ m_snap_clusters.clear();
+ }
+}
+
+uint64_t
+BeesBtrfsExtentLayer::snap_cluster(uint64_t root) const
+{
+ BEESTRACE("snap_cluster root " << root);
+ unique_lock<mutex> lock(m_snap_cluster_mutex);
+ if (!m_snap_clusters_built) {
+ BEESNOTE("building snapshot cluster map");
+ // One ROOT_ITEM sweep of the root tree. Only subvol roots
+ // (the top-level fs tree and ids in the first-free range)
+ // participate; per-fs internal trees also have ROOT_ITEMs
+ // but never own file extent refs.
+ vector<ScanNextSnapRoot> roots;
+ BtrfsRootFetcher fetcher(m_ctx.root_fd());
+ fetcher.type(BTRFS_ROOT_ITEM_KEY);
+ for (auto bti = fetcher.lower_bound(BTRFS_FS_TREE_OBJECTID);
+ !!bti; bti = fetcher.next(bti.objectid())) {
+ const auto objectid = bti.objectid();
+ if (objectid == BTRFS_FS_TREE_OBJECTID ||
+ (objectid >= BTRFS_FIRST_FREE_OBJECTID &&
+ objectid <= BTRFS_LAST_FREE_OBJECTID)) {
+ roots.push_back(ScanNextSnapRoot{
+ objectid, bti.root_uuid(),
+ bti.root_parent_uuid() });
+ }
+ if (objectid >= BTRFS_LAST_FREE_OBJECTID) {
+ break;
+ }
+ }
+ m_snap_clusters = scan_next_snap_cluster_map(roots);
+ m_snap_clusters_built = true;
+ BEESLOGDEBUG("snap_cluster map built: " << roots.size()
+ << " subvols, " << m_snap_clusters.size()
+ << " in multi-member families");
+ BEESCOUNT(layer_snap_cluster_build);
+ }
+ const auto found = m_snap_clusters.find(root);
+ return found == m_snap_clusters.end() ? 0 : found->second;
}
// BeesBtrfsExtentLayer::extent_at_bytenr() is defined in
Fd BeesOverlayLayer::open_ref(const BeesRef &ref) const { return m_parent->open_ref(ref); }
+uint64_t
+BeesOverlayLayer::snap_cluster(uint64_t root) const
+{
+ // The btrfs base owns the map; overlays add nothing — planned
+ // mutations never create or destroy subvols.
+ return m_parent->snap_cluster(root);
+}
+
void BeesOverlayLayer::readonly_fetch(const BeesRef &ref) const { m_parent->readonly_fetch(ref); }
void BeesOverlayLayer::extent_data_fetch(const BeesRef &ref) const { m_parent->extent_data_fetch(ref); }
void BeesOverlayLayer::eof_fetch(const BeesRef &ref) const { m_parent->eof_fetch(ref); }
/// do not cache).
virtual bool has_cached_extent(uint64_t) const { return false; }
+ // -- Snapshot families --
+
+ /// Return the snapshot-family cluster id of subvol @p root:
+ /// nonzero and shared by every member of one snapshot family
+ /// (subvols connected through root_item uuid / parent_uuid links)
+ /// with at least two existing members; 0 when the subvol shares
+ /// metadata pages with no other existing subvol. Consumed by the
+ /// snapshot-sharing space-debt term, which groups a dst's refs by
+ /// (cluster, inode, offset) to find refs that are one physical
+ /// shared metadata item. Default: 0 (no snapshot families).
+ virtual uint64_t snap_cluster(uint64_t) const { return 0; }
+
// -- Cache invalidation --
/// Invalidate all cached state for the extent at @p bytenr.
Fd open_ref(const BeesRef &ref) const override;
+ /// Lazy lookup in the snapshot-family cluster map, built on first
+ /// query from one ROOT_ITEM sweep of the root tree and bulk-cleared
+ /// at transid change — snapshot creation and deletion only become
+ /// visible at a commit, so per-transid granularity is exact.
+ uint64_t snap_cluster(uint64_t root) const override;
+
/// Per-extent cache eviction. Removes the entry for @p bytenr
/// from the base extent cache so that the next lookup re-reads
/// the authoritative state from btrfs.
/// releases the refs immediately.
mutable map<pair<uint64_t, uint64_t>,
map<uint64_t, BeesRef>> m_inode_ref_cache;
+
+ /// Guards m_snap_clusters + m_snap_clusters_built. Unlike
+ /// m_cache_mutex this IS held across the build's TREE_SEARCH
+ /// sweep: the map is built at most once per transid cycle and
+ /// only snap_cluster() callers block on it.
+ mutable mutex m_snap_cluster_mutex;
+ mutable bool m_snap_clusters_built = false;
+
+ /// root id -> snapshot-family cluster id, multi-member families
+ /// only (scan_next_snap_cluster_map). Absence means "shares
+ /// metadata pages with no other subvol". Bulk-cleared at transid
+ /// change like the other layer caches.
+ mutable map<uint64_t, uint64_t> m_snap_clusters;
};
/// Per-planner overlay layer — ephemeral working set on top of a base.
Fd open_ref(const BeesRef &ref) const override;
+ /// Delegates to the parent layer (the btrfs base owns the map).
+ uint64_t snap_cluster(uint64_t root) const override;
+
// -- Ref map queries --
/// Look up which extent owns a given ref. Returns 0 if unknown.
#include "crucible/string.h" // to_hex, pretty
#include <algorithm>
+#include <functional>
using namespace crucible;
using namespace std;
return c;
}
+map<uint64_t, uint64_t>
+scan_next_snap_cluster_map(const vector<ScanNextSnapRoot> &roots)
+{
+ using Uuid = array<uint8_t, 16>;
+ static const Uuid zero_uuid = {};
+
+ // Union-find over uuid values. Elements exist implicitly (an
+ // unseen uuid is its own set); joins insert parent pointers, find
+ // path-compresses. Joining uuid values rather than subvols makes
+ // the sibling rule automatic: two children of a deleted parent both
+ // join the parent's uuid, which needs no surviving subvol to exist.
+ map<Uuid, Uuid> up;
+ function<Uuid(const Uuid &)> find = [&up, &find](const Uuid &x) -> Uuid {
+ const auto it = up.find(x);
+ if (it == up.end() || it->second == x) {
+ return x;
+ }
+ const auto root = find(it->second);
+ it->second = root;
+ return root;
+ };
+
+ for (const auto &r : roots) {
+ if (r.m_uuid == zero_uuid || r.m_parent_uuid == zero_uuid) {
+ continue;
+ }
+ const auto ra = find(r.m_uuid);
+ const auto rb = find(r.m_parent_uuid);
+ if (!(ra == rb)) {
+ up[ra] = rb;
+ }
+ }
+
+ // Collect each family's subvol members. A zero-uuid legacy subvol
+ // can still be a member through its parent_uuid; with neither field
+ // it has no identity to cluster on.
+ map<Uuid, vector<uint64_t>> members;
+ for (const auto &r : roots) {
+ if (!(r.m_uuid == zero_uuid)) {
+ members[find(r.m_uuid)].push_back(r.m_root_id);
+ } else if (!(r.m_parent_uuid == zero_uuid)) {
+ members[find(r.m_parent_uuid)].push_back(r.m_root_id);
+ }
+ }
+
+ // Publish only multi-member families — the subvols that can share
+ // metadata pages with another existing subvol. The cluster id is
+ // the family's lowest root id: stable within one sweep, nonzero by
+ // construction (root ids start at BTRFS_FS_TREE_OBJECTID).
+ map<uint64_t, uint64_t> rv;
+ for (const auto &fam : members) {
+ if (fam.second.size() < 2) {
+ continue;
+ }
+ const auto cluster_id =
+ *min_element(fam.second.begin(), fam.second.end());
+ for (const auto root_id : fam.second) {
+ rv[root_id] = cluster_id;
+ }
+ }
+ return rv;
+}
+
+uint64_t
+scan_next_snap_extra(vector<array<uint64_t, 3>> keys)
+{
+ // sum (k - 1) over groups == total keys - distinct keys.
+ sort(keys.begin(), keys.end());
+ const auto distinct = unique(keys.begin(), keys.end()) - keys.begin();
+ return keys.size() - static_cast<uint64_t>(distinct);
+}
+
size_t
scan_next_region_block_count(bool unreachable, uint64_t byte_count,
uint64_t block_size)
#include "bees.h" // BeesRewritePolicy
#include "bees-extent.h" // BeesExtent
+#include <array>
#include <cstdint>
#include <list>
+#include <map>
#include <memory>
#include <optional>
#include <ostream>
bool dst_compressed, uint64_t dst_phys_size,
uint64_t good_blocks, uint64_t clone_alignment);
+/// One subvol's identity for snapshot-family clustering
+/// (snapshot-metadata-cost.md §2): the root id plus the uuid and
+/// parent_uuid octets from its ROOT_ITEM. Zero-filled uuids mean the
+/// field is absent (legacy short root item, or not a snapshot).
+struct ScanNextSnapRoot {
+ uint64_t m_root_id = 0;
+ std::array<uint8_t, 16> m_uuid = {};
+ std::array<uint8_t, 16> m_parent_uuid = {};
+};
+
+/// Compute the snapshot-family cluster map (snapshot-metadata-cost.md §2)
+/// from every existing subvol's ROOT_ITEM identity. Subvols are joined
+/// into one family through uuid links: a subvol's own uuid and its nonzero
+/// parent_uuid name the same shared page lineage, so union-find over uuid
+/// values connects parents to children and siblings to each other even
+/// when the common parent has been deleted (its uuid survives as the
+/// phantom join point in the children's parent_uuid). A deleted middle
+/// link (snapshot-of-a-snapshot chains) breaks the join — the accepted
+/// miss; see the design note.
+///
+/// Returns root_id -> cluster_id for subvols whose family has at least
+/// two existing members — the subvols that can share metadata pages with
+/// another subvol. Singleton-family subvols are absent (callers read
+/// absence as "no sharing"), so the common no-snapshots filesystem costs
+/// one empty map. cluster_id is the lowest member root_id, nonzero by
+/// construction. Zero-uuid legacy subvols participate only through a
+/// nonzero parent_uuid; with neither field they cannot be clustered.
+std::map<uint64_t, uint64_t>
+scan_next_snap_cluster_map(const std::vector<ScanNextSnapRoot> &roots);
+
+/// Snapshot-sharing metadata undercharge (snapshot-metadata-cost.md §3):
+/// sum of (k - 1) over groups of identical keys. Each key is a
+/// (cluster_id, inode, offset) triple for one dst ref whose subvol
+/// belongs to a multi-member snapshot family; k identical keys are one
+/// physical metadata item seen through k subvols, so replacing the group
+/// writes k private copies but removes only the one shared item — the
+/// per-logical-ref accounting undercharges it by exactly k - 1,
+/// independent of the fragment count. Refs outside any family (cluster
+/// 0) must not be passed in; their accounting is already correct.
+uint64_t
+scan_next_snap_extra(std::vector<std::array<uint64_t, 3>> keys);
+
/// 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
assert(buggy.space_debt(policy) == -3787.0);
}
+// ----------------------------------------------------------------------
+// Snapshot-family clustering and the snapshot-sharing undercharge
+// (snapshot-metadata-cost.md §2/§3).
+
+static array<uint8_t, 16>
+uuid_of(uint8_t tag)
+{
+ array<uint8_t, 16> rv = {};
+ rv[0] = tag;
+ return rv;
+}
+
+static void
+test_snap_cluster_map()
+{
+ const auto zero = array<uint8_t, 16>{};
+
+ // Star: parent A (root 256) with snapshots B (257) and C (258).
+ // One family, cluster id = lowest member root id.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 256, uuid_of(1), zero },
+ { 257, uuid_of(2), uuid_of(1) },
+ { 258, uuid_of(3), uuid_of(1) },
+ });
+ assert(m.size() == 3);
+ assert(m.at(256) == 256);
+ assert(m.at(257) == 256);
+ assert(m.at(258) == 256);
+ }
+
+ // Orphan siblings: Y and Z snapshotted from deleted X. Their
+ // parent_uuid is the phantom join point; both cluster together.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 300, uuid_of(4), uuid_of(9) },
+ { 301, uuid_of(5), uuid_of(9) },
+ });
+ assert(m.size() == 2);
+ assert(m.at(300) == 300);
+ assert(m.at(301) == 300);
+ }
+
+ // Sole survivor: snapshot whose parent is deleted, no siblings.
+ // Singleton family — absent from the map (no sharing possible).
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 400, uuid_of(6), uuid_of(9) },
+ { 401, uuid_of(7), zero },
+ });
+ assert(m.empty());
+ }
+
+ // Chain D -> E -> F, all present: transitively one family.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 500, uuid_of(1), zero },
+ { 501, uuid_of(2), uuid_of(1) },
+ { 502, uuid_of(3), uuid_of(2) },
+ });
+ assert(m.size() == 3);
+ assert(m.at(502) == 500);
+ }
+
+ // Chain with deleted middle: D and F survive but no surviving uuid
+ // links them — the accepted miss (both are singleton families).
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 500, uuid_of(1), zero },
+ { 502, uuid_of(3), uuid_of(2) },
+ });
+ assert(m.empty());
+ }
+
+ // Two independent families stay distinct clusters.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 256, uuid_of(1), zero },
+ { 257, uuid_of(2), uuid_of(1) },
+ { 600, uuid_of(3), zero },
+ { 601, uuid_of(4), uuid_of(3) },
+ });
+ assert(m.at(256) == 256 && m.at(257) == 256);
+ assert(m.at(600) == 600 && m.at(601) == 600);
+ }
+
+ // Legacy zero-uuid subvols: no identity, never clustered — and the
+ // zero uuid must not act as a shared join value between them.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 700, zero, zero },
+ { 701, zero, zero },
+ });
+ assert(m.empty());
+ }
+
+ // Zero-uuid subvols with the same nonzero parent_uuid are still
+ // siblings — membership through parent_uuid alone.
+ {
+ const auto m = scan_next_snap_cluster_map({
+ { 800, zero, uuid_of(8) },
+ { 801, zero, uuid_of(8) },
+ });
+ assert(m.size() == 2);
+ assert(m.at(800) == 800);
+ assert(m.at(801) == 800);
+ }
+}
+
+static void
+test_snap_extra_grouping()
+{
+ // No clustered refs: no undercharge.
+ assert(scan_next_snap_extra({}) == 0);
+
+ // One group of k=4 (the design's worked cases at 4 sharing
+ // subvols): extra = k - 1 = 3.
+ assert(scan_next_snap_extra({
+ { 1, 10, 0 }, { 1, 10, 0 }, { 1, 10, 0 }, { 1, 10, 0 },
+ }) == 3);
+
+ // Two k=4 groups (two refs replaced): 3 + 3 = 6.
+ assert(scan_next_snap_extra({
+ { 1, 10, 0 }, { 1, 10, 0 }, { 1, 10, 0 }, { 1, 10, 0 },
+ { 1, 10, 4096 }, { 1, 10, 4096 }, { 1, 10, 4096 }, { 1, 10, 4096 },
+ }) == 6);
+
+ // k=1 groups contribute nothing: a ref unique within its family
+ // (the others diverged that file) is charged normally.
+ assert(scan_next_snap_extra({
+ { 1, 10, 0 }, { 1, 11, 0 }, { 1, 10, 8192 },
+ }) == 0);
+
+ // Same (inode, offset) in different clusters must not merge —
+ // unrelated snapshot families share no pages.
+ assert(scan_next_snap_extra({
+ { 1, 10, 0 }, { 2, 10, 0 },
+ }) == 0);
+
+ // Mixed: one k=3 group plus singletons.
+ assert(scan_next_snap_extra({
+ { 1, 10, 0 }, { 1, 10, 0 }, { 1, 10, 0 },
+ { 1, 12, 0 }, { 2, 10, 0 },
+ }) == 2);
+}
+
int
main(int, char **)
{
RUN_A_TEST(test_ref_op_cost_prices_whole_extent_dedupe());
RUN_A_TEST(test_debt_fields_shared_builder());
RUN_A_TEST(test_search_prefers_high_ref_src());
+ RUN_A_TEST(test_snap_cluster_map());
+ RUN_A_TEST(test_snap_extra_grouping());
return 0;
}