]> git.hungrycats.org Git - bees/commitdiff
rewrite: make the canonical-src tie-break configurable (prefer-canonical-src)
authorZygo Blaxell <bees@furryterror.org>
Thu, 9 Jul 2026 15:33:20 +0000 (11:33 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:04:13 +0000 (00:04 -0400)
The equal-reach source tie-break — prefer the source with the most references
so duplicates consolidate onto a canonical extent — has been unconditional.  It
is only safe when ref-op-cost prices the reference movement that consolidation
drives: with ref-op-cost = 0 (the default) it concentrates references onto a few
extents up to refs-max with nothing to bound the resulting re-canonicalization,
doing far more reference-rewriting work than spreading references across sources
would.  A t100 / ref-op-cost = 0 run showed ~70x the reference redirects of a
build without the tie-break, piled onto the refs-max ceiling.

Add a boolean rewrite.prefer-canonical-src so the tie-break can be enabled or
disabled — for an A/B test confirming it is the cause, and as the knob operators
use to pair it with a non-zero ref-op-cost.  When disabled the flattener leaves
each match's src ref count at 0, which the coverage search already reads as "no
preference" (ties resolve first-match, as before) and which also skips the
per-candidate refs() lookup.  Default yes preserves current behavior; the docs
and config comment note it is only safe with a non-zero ref-op-cost.

Assisted-by: Claude-Code:claude-opus-4-8
docs/config-file.md
src/bees-config-v2.cc
src/bees-config.cc
src/bees-scan-next.cc
src/bees.h
test/test-bees-plan.cc

index ee77450d84e0e6af41f2898a947ef2387ff4879a..ce890eb09fab6e428d0d2f25ff093caa3f4b981d 100644 (file)
@@ -291,6 +291,23 @@ removed.
     unchanged until an operator opts in).
   * Accepts a [size value](config-format.md#size-values).
 
+* **`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
+  the source that already has the most references.  This consolidates duplicates
+  onto a single canonical extent (a star topology) rather than spreading them
+  across arbitrary sources; when off, the first equally-good source is used and
+  references spread out (the historical behavior).
+  * Only safe with a non-zero [`ref-op-cost`](#rewrite-section).  Consolidation
+    concentrates references onto a few extents, and `ref-op-cost` is what prices
+    — and therefore bounds — the reference movement that concentration drives.
+    With `ref-op-cost = 0` there is no such bound: references pile onto canonical
+    extents up to `refs-max` and re-canonicalize along a chain, doing far more
+    reference-rewriting work than spreading them would.  Enable this only
+    together with a non-zero `ref-op-cost`.
+  * Default: `yes`.
+  * Accepts a [boolean value](config-format.md#boolean-values).
+
 * **`plan-max-vertices`**
   Boundary-vertex budget for the shortest-path coverage search.  The exact
   search is O(V²) in the boundary count V (the distinct match endpoints of a
index befb4e1443ff459e2db851a41fd6f40e60d470e0..b9b2da058de62d3ab4109c107909fe4810c85ef0 100644 (file)
@@ -194,6 +194,11 @@ static const char bees_config_v2[] = R"--v2-config--(
         # Size value in bytes; 0 disables.  See docs/config-file.md.
         ref-op-cost = 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.
+        prefer-canonical-src = yes
+
         # Boundary-vertex budget for the coverage search.  Above this V the
         # planner uses a linear greedy cover instead of the O(V^2) exact
         # search.  Integer count, or unlimited to disable the fallback.
index 06185d7f99252f8483e60ce3070dac8ce9454c7c..03086ff4f0a88e17e7d29b048a05039ec1aada00 100644 (file)
@@ -492,6 +492,7 @@ BeesConfig::load_rewrite_policy()
        rv.m_inode_locking       = load("rewrite.inode-locking",       bees_parse_bool);
        rv.m_keep_older          = load("rewrite.keep-older",          bees_parse_bool);
        rv.m_keep_newer          = load("rewrite.keep-newer",          bees_parse_bool);
+       rv.m_prefer_canonical_src = load("rewrite.prefer-canonical-src", bees_parse_bool);
        {
                BEESTRACE("loading rewrite.read-size");
                rv.m_read_size = get("rewrite.read-size", bees_parse_size);
index 113b17c14959cf774760b92af0017a6bc577d756..38cd1a0e194632d262415073ee20eb2e038261ae 100644 (file)
@@ -1075,10 +1075,13 @@ scan_next_choose_match_plan(const BeesExtent &dst,
                for (const auto &match : group.m_matches) {
                        if (match.size() == 0) continue;
                        // Carry the src ref count as the search's equal-reach tie-break
-                       // (prefer consolidating onto the higher-ref, canonical src).  The
-                       // refs() lookup is layer-cached; the src extents here were already
-                       // resolved to produce these matches, so this is warm.
-                       const uint64_t src_refs = match.m_src.refs(layer)->size();
+                       // (rewrite.prefer-canonical-src): prefer consolidating onto the
+                       // higher-ref, canonical src.  When disabled leave it 0, which the
+                       // search reads as "no preference" (ties resolve as before) and
+                       // also skips the refs() lookup.  The lookup is layer-cached; the
+                       // 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;
                        in.m_matches.push_back(PlanSearchMatch{
                                match.m_dst_begin, match.m_dst_end, flat.size(), src_refs });
                        flat.push_back(FlatMatch{ &group, &match });
index 12ad7ac2f7bfed2ec99c31a9424c11bf6aaa95db..12308aa6815510d77fe6b3b792542d81860da3ef 100644 (file)
@@ -1471,6 +1471,17 @@ struct BeesRewritePolicy {
        /// 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;
+       /// 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
+       /// arbitrary one; off spreads references across sources (the historical
+       /// behavior).  Consolidating is only safe when reference moves are priced:
+       /// with rewrite.ref-op-cost = 0 it concentrates references onto a few
+       /// extents with nothing to bound the resulting re-canonicalization, so pair
+       /// it with a non-zero ref-op-cost.  Authoritative default in
+       /// bees-config-v2.cc; the C++ initializer (false) is only the pre-load
+       /// value (a bool has no invalid sentinel — see code that sets it).
+       bool m_prefer_canonical_src = false;
        /// Boundary-vertex budget for the shortest-path coverage search.
        /// The exact search is O(V^2) in the boundary count V and holds the
        /// dst extent's Exclusion for its whole duration, so a high-V extent
index 49f25d8c55aa5bc3d0ecede4575afd59ab3b8b21..1c9d892b197acd792113531c4b61049e5b25fc98 100644 (file)
@@ -53,6 +53,7 @@ default_policy()
        p.m_inode_locking         = true;
        p.m_keep_older            = true;
        p.m_keep_newer            = true;
+       p.m_prefer_canonical_src  = true;
        p.ref_cost                = 53;
        p.extent_cost             = 53;
        p.m_ref_op_cost           = 0;