@@ -634,6 +634,7 @@ where
634634 let bitmap_batch = BitmapBatch :: Layer ( Arc :: new ( BitmapBatchLayer {
635635 parent : bitmap_parent. clone ( ) ,
636636 overlay : Arc :: new ( overlay) ,
637+ shared : Arc :: clone ( bitmap_parent. shared ( ) ) ,
637638 } ) ) ;
638639
639640 // Compute canonical root. The grafted batch alone cannot resolve committed nodes,
@@ -763,19 +764,19 @@ pub(crate) struct BitmapBatchLayer<const N: usize> {
763764 pub ( crate ) parent : BitmapBatch < N > ,
764765 /// Chunk-level overlay: materialized bytes for every chunk that differs from parent.
765766 pub ( crate ) overlay : Arc < ChunkOverlay < N > > ,
767+ /// Cached terminal [`SharedBitmap`] so [`BitmapBatch::shared`] and
768+ /// [`BitmapBatch::pruned_chunks`] answer in O(1) instead of walking the chain.
769+ pub ( crate ) shared : Arc < SharedBitmap < N > > ,
766770}
767771
768772impl < const N : usize > BitmapBatch < N > {
769773 const CHUNK_SIZE_BITS : u64 = BitMap :: < N > :: CHUNK_SIZE_BITS ;
770774
771- /// Walk to the terminal [`SharedBitmap`] at the bottom of the chain.
775+ /// Return the terminal [`SharedBitmap`] at the bottom of the chain.
772776 fn shared ( & self ) -> & Arc < SharedBitmap < N > > {
773- let mut current = self ;
774- loop {
775- match current {
776- Self :: Base ( s) => return s,
777- Self :: Layer ( layer) => current = & layer. parent ,
778- }
777+ match self {
778+ Self :: Base ( s) => s,
779+ Self :: Layer ( layer) => & layer. shared ,
779780 }
780781 }
781782
@@ -817,6 +818,7 @@ impl<const N: usize> BitmapBatch<N> {
817818 return Self :: Layer ( Arc :: new ( BitmapBatchLayer {
818819 parent : Self :: Base ( Arc :: clone ( shared) ) ,
819820 overlay : Arc :: clone ( & layer. overlay ) ,
821+ shared : Arc :: clone ( shared) ,
820822 } ) ) ;
821823 }
822824 }
@@ -835,6 +837,7 @@ impl<const N: usize> BitmapBatch<N> {
835837 result = Self :: Layer ( Arc :: new ( BitmapBatchLayer {
836838 parent : result,
837839 overlay,
840+ shared : Arc :: clone ( shared) ,
838841 } ) ) ;
839842 }
840843 result
@@ -879,13 +882,7 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
879882 }
880883
881884 fn pruned_chunks ( & self ) -> usize {
882- let mut current = self ;
883- loop {
884- match current {
885- Self :: Base ( shared) => return shared. pruned_chunks ( ) ,
886- Self :: Layer ( layer) => current = & layer. parent ,
887- }
888- }
885+ self . shared ( ) . pruned_chunks ( )
889886 }
890887
891888 fn len ( & self ) -> u64 {
@@ -1416,4 +1413,127 @@ mod tests {
14161413 // Empty bitmap, tip = 0: no candidates.
14171414 assert_eq ! ( scan. next_candidate( Location :: new( 0 ) , 0 ) , None ) ;
14181415 }
1416+
1417+ // ---- trim_committed tests ----
1418+ //
1419+ // `trim_committed` is called from `MerkleizedBatch::new_batch` to strip any `Layer`s whose
1420+ // overlays have already been absorbed into the shared committed bitmap by a prior apply.
1421+ // It has five branches; each test below exercises one of them directly, without going
1422+ // through the full Db/batch machinery, so the function's structural output can be asserted.
1423+
1424+ /// Build a chain `Base(shared) -> Layer(len=L1) -> Layer(len=L2) -> ...` from a list of
1425+ /// overlay lengths (bottom to top). Each constructed `Layer` caches `shared` per the
1426+ /// struct's invariant.
1427+ fn make_chain ( shared : & Arc < SharedBitmap < N > > , overlay_lens : & [ u64 ] ) -> BitmapBatch < N > {
1428+ let mut chain = BitmapBatch :: Base ( Arc :: clone ( shared) ) ;
1429+ for & len in overlay_lens {
1430+ chain = BitmapBatch :: Layer ( Arc :: new ( BitmapBatchLayer {
1431+ parent : chain,
1432+ overlay : Arc :: new ( ChunkOverlay :: new ( len) ) ,
1433+ shared : Arc :: clone ( shared) ,
1434+ } ) ) ;
1435+ }
1436+ chain
1437+ }
1438+
1439+ /// Walk a chain and return its overlay lengths in bottom-to-top order. Used to assert the
1440+ /// structural output of `trim_committed` without touching private fields. Panics if the
1441+ /// chain isn't terminated by a single `Base` at the bottom.
1442+ fn chain_overlays ( batch : & BitmapBatch < N > ) -> Vec < u64 > {
1443+ let mut lens = Vec :: new ( ) ;
1444+ let mut current = batch;
1445+ while let BitmapBatch :: Layer ( layer) = current {
1446+ lens. push ( layer. overlay . len ) ;
1447+ current = & layer. parent ;
1448+ }
1449+ assert ! ( matches!( current, BitmapBatch :: Base ( _) ) ) ;
1450+ lens. reverse ( ) ;
1451+ lens
1452+ }
1453+
1454+ /// Branch 1: input is already a bare `Base` with no speculative layers on top. This is the
1455+ /// input `MerkleizedBatch::new_batch` sees when a caller builds a child off a batch whose
1456+ /// chain was previously trimmed flat (e.g., immediately after apply collapsed everything).
1457+ /// `trim_committed` must short-circuit and return an equivalent `Base` pointing at the
1458+ /// same `SharedBitmap`; no new allocation should be needed for the terminal.
1459+ #[ test]
1460+ fn trim_committed_already_base ( ) {
1461+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1462+ let base = BitmapBatch :: Base ( Arc :: clone ( & shared) ) ;
1463+ let result = base. trim_committed ( ) ;
1464+ // Must still be `Base`, and pointer-equal to the input's shared terminal.
1465+ match result {
1466+ BitmapBatch :: Base ( s) => assert ! ( Arc :: ptr_eq( & s, & shared) ) ,
1467+ BitmapBatch :: Layer ( _) => panic ! ( "expected Base" ) ,
1468+ }
1469+ }
1470+
1471+ /// Branch 2: the shared committed bitmap has caught up to (or past) the top layer's
1472+ /// `overlay.len`, meaning every layer in the chain has been absorbed by prior applies.
1473+ /// This is the steady-state "extend a just-applied batch" flow: after `apply_batch(A)`,
1474+ /// `A`'s own layer has `overlay.len == committed`. `trim_committed` must collapse the
1475+ /// entire chain down to a bare `Base` so the new child starts from a clean terminal.
1476+ #[ test]
1477+ fn trim_committed_all_committed ( ) {
1478+ // `shared.len() == 64`; the single layer's `overlay.len == 32 (<= 64)`, so it's committed.
1479+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1480+ let chain = make_chain ( & shared, & [ 32 ] ) ;
1481+ let result = chain. trim_committed ( ) ;
1482+ // Collapsed to a bare Base, pointing at the original shared.
1483+ match result {
1484+ BitmapBatch :: Base ( s) => assert ! ( Arc :: ptr_eq( & s, & shared) ) ,
1485+ BitmapBatch :: Layer ( _) => panic ! ( "expected Base after full trim" ) ,
1486+ }
1487+ }
1488+
1489+ /// Branch 3: every layer in the chain is still speculative (none have been applied), so
1490+ /// the committed bitmap hasn't caught up to any layer's `overlay.len`. `trim_committed`
1491+ /// must return the chain unchanged. This is the "speculate ahead without applying" case:
1492+ /// a caller building multiple batches deep (A, then B off A, then C off B) without
1493+ /// `apply_batch` in between.
1494+ #[ test]
1495+ fn trim_committed_none_committed ( ) {
1496+ // `shared.len() == 32`; both overlays have `len > 32`, so neither is committed.
1497+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 32 ] ) ) ) ;
1498+ let chain = make_chain ( & shared, & [ 64 , 96 ] ) ;
1499+ let result = chain. trim_committed ( ) ;
1500+ // Structure must be preserved in bottom-to-top order.
1501+ assert_eq ! ( chain_overlays( & result) , vec![ 64 , 96 ] ) ;
1502+ }
1503+
1504+ /// Branch 4: exactly one layer is uncommitted (the newest), and everything below it has
1505+ /// been absorbed. This is the dominant pattern in chained growth: apply parent A, then
1506+ /// B held alive off A, then call `B.new_batch()` to build C. Roberto's fast path avoids
1507+ /// the general-case allocation dance and constructs the result directly as
1508+ /// `Layer(Base, overlay_B)` with a single `Arc::new`. The test also verifies the rebuilt
1509+ /// layer carries the cached `shared` reference correctly.
1510+ #[ test]
1511+ fn trim_committed_exactly_one_uncommitted ( ) {
1512+ // `shared.len() == 64`; committed layer (`overlay.len == 64`) + uncommitted (`96`).
1513+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1514+ let chain = make_chain ( & shared, & [ 64 , 96 ] ) ;
1515+ let result = chain. trim_committed ( ) ;
1516+ // The committed layer is gone; only the uncommitted overlay remains.
1517+ assert_eq ! ( chain_overlays( & result) , vec![ 96 ] ) ;
1518+ // And the rebuilt layer's `shared` field still points at the original terminal.
1519+ assert ! ( Arc :: ptr_eq( result. shared( ) , & shared) ) ;
1520+ }
1521+
1522+ /// Branch 5: two or more uncommitted layers on top of a committed prefix. This is the
1523+ /// general-case path: the function has to walk the uncommitted tail, collect the overlays,
1524+ /// and rebuild a fresh chain rooted at `Base`. Exercises the loop that wires the cached
1525+ /// `shared` field into each reconstructed layer. Real-world trigger: build A, then B off A,
1526+ /// then C off B; apply only A; then call `C.new_batch()` — the trim sees B and C as
1527+ /// uncommitted, with A already absorbed.
1528+ #[ test]
1529+ fn trim_committed_multiple_uncommitted ( ) {
1530+ // `shared.len() == 64`; committed layer (64), then two uncommitted (96, 128).
1531+ let shared = Arc :: new ( SharedBitmap :: < N > :: new ( make_bitmap ( & [ true ; 64 ] ) ) ) ;
1532+ let chain = make_chain ( & shared, & [ 64 , 96 , 128 ] ) ;
1533+ let result = chain. trim_committed ( ) ;
1534+ // Committed layer dropped; uncommitted pair preserved in order.
1535+ assert_eq ! ( chain_overlays( & result) , vec![ 96 , 128 ] ) ;
1536+ // Every reconstructed layer must still cache the original shared terminal.
1537+ assert ! ( Arc :: ptr_eq( result. shared( ) , & shared) ) ;
1538+ }
14191539}
0 commit comments