]> git.hungrycats.org Git - bees/commitdiff
hash: derive the occupancy survey from per-extent fragments
authorZygo Blaxell <bees@furryterror.org>
Wed, 17 Jun 2026 00:42:32 +0000 (20:42 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:03:58 +0000 (00:03 -0400)
The prefetch-thread survey walked every cell of the table each interval to
build the page-occupancy histogram, counting both occupancy and a per-cell
type breakdown (compressed / unaligned_eof / toxic).  With 256 cells per
bucket that is a full-table scan under per-extent locks every hour, and the
type breakdown reads v1 BeesAddress bits that scan-next neither sets on store
nor reads on fetch — so under scan-next those counters are structurally
meaningless.

Replace the walk with a cached per-extent occupancy fragment.  Each
ExtentMetaData gains a small bucket-fill histogram (index = occupied cells in
a bucket, value = number of buckets at that occupancy).  It is rebuilt under
the extent lock at two points that already touch the whole extent:  the
startup load, and each writeback (where the extent is copied for pwrite).
Occupancy can only change when a cell is inserted or evicted, which dirties
the extent, so a writeback-time refresh captures every change; read-mostly
extents keep their load-time fragment.  Staleness is bounded by the writeback
cycle, so the printed graph is effectively current with on-disk data.

The survey is now a sum over fragments with no cell walk and no locking
beyond a brief per-extent read.  Drop the type breakdown and the percent()
helper that only formatted it; total cell count comes from m_cells directly.
verify_cell_range loses its last live caller but is left in place (now
referenced only from #if 0 blocks); removing it is a separate cleanup.

Assisted-by: Claude-Code:claude-opus-4-8
Signed-off-by: Zygo Blaxell <bees@furryterror.org>
src/bees-hash.cc
src/bees.h

index cf23246f3e1e8d48d5f6de30707e26fb11da57bd..1dc206d152393fd524bee9c1ed6ef9b168017b8e 100644 (file)
@@ -120,6 +120,27 @@ BeesHashTable::get_extent_range(HashType hash)
        return make_pair(bp, ep);
 }
 
+void
+BeesHashTable::recompute_extent_fill_histogram_locked(uint64_t extent_index)
+{
+       // Caller holds the extent lock (or runs single-threaded at startup).
+       // Rebuild this extent's per-bucket occupancy histogram from the live
+       // cells.  Cheap (c_buckets_per_extent buckets) and it rides on work the
+       // caller already does under the lock — the startup load and each
+       // writeback — so the periodic survey never has to walk cells itself.
+       auto &hist = m_extent_metadata.at(extent_index).m_fill_histogram;
+       hist.fill(0);
+       for (Bucket *bucket = m_extent_ptr[extent_index].p_buckets; bucket < m_extent_ptr[extent_index + 1].p_buckets; ++bucket) {
+               size_t occupied = 0;
+               for (Cell *cell = bucket[0].p_cells; cell < bucket[1].p_cells; ++cell) {
+                       if (cell->e_addr) {
+                               ++occupied;
+                       }
+               }
+               ++hist.at(occupied);
+       }
+}
+
 bool
 BeesHashTable::flush_dirty_extent(uint64_t extent_index)
 {
@@ -128,6 +149,11 @@ BeesHashTable::flush_dirty_extent(uint64_t extent_index)
        auto lock = lock_extent_by_index(extent_index);
        bool wrote_extent = false;
 
+       // Refresh the occupancy fragment while we hold the lock and the extent
+       // is about to be persisted; the survey sums these instead of walking
+       // cells.
+       recompute_extent_fill_histogram_locked(extent_index);
+
        if (!m_persistent) {
                // Memory-only mode: mark clean without any file I/O.
                m_extent_metadata.at(extent_index).m_dirty = false;
@@ -262,17 +288,6 @@ BeesHashTable::writeback_loop()
        BEESLOGDEBUG("Exited hash table writeback_loop");
 }
 
-static
-string
-percent(size_t num, size_t den)
-{
-       if (den) {
-               return astringprintf("%u%%", num * 100 / den);
-       } else {
-               return "--%";
-       }
-}
-
 void
 BeesHashTable::prefetch_loop()
 {
@@ -281,47 +296,22 @@ BeesHashTable::prefetch_loop()
                size_t width = 64;
                vector<size_t> occupancy(width, 0);
                size_t occupied_count = 0;
-               size_t total_count = 0;
-               size_t compressed_count = 0;
-               size_t compressed_offset_count = 0;
-               size_t toxic_count = 0;
-               size_t unaligned_eof_count = 0;
+               const size_t total_count = m_cells;
 
+               // Sum the per-extent occupancy fragments maintained at load and
+               // writeback — no cell walk.  Each extent contributes its cached
+               // bucket-fill histogram.  The old type breakdowns (compressed,
+               // unaligned_eof, toxic) are gone: those were v1 BeesAddress bits
+               // that scan-next neither sets on store nor reads on fetch.
                for (uint64_t ext = 0; ext < m_extents && !m_stop_requested; ++ext) {
                        catch_all([&]() {
-                               BEESNOTE("analyzing hash table extent #" << ext << " of " << m_extents);
-                               bool duplicate_bugs_found = false;
+                               BEESNOTE("summarizing hash table extent #" << ext << " of " << m_extents);
                                auto lock = lock_extent_by_index(ext);
-                               for (Bucket *bucket = m_extent_ptr[ext].p_buckets; bucket < m_extent_ptr[ext + 1].p_buckets; ++bucket) {
-                                       if (verify_cell_range(bucket[0].p_cells, bucket[1].p_cells)) {
-                                               duplicate_bugs_found = true;
-                                       }
-                                       size_t this_bucket_occupied_count = 0;
-                                       for (Cell *cell = bucket[0].p_cells; cell < bucket[1].p_cells; ++cell) {
-                                               if (cell->e_addr) {
-                                                       ++this_bucket_occupied_count;
-                                                       BeesAddress a(cell->e_addr);
-                                                       if (a.is_compressed()) {
-                                                               ++compressed_count;
-                                                               if (a.has_compressed_offset()) {
-                                                                       ++compressed_offset_count;
-                                                               }
-                                                       }
-                                                       if (a.is_toxic()) {
-                                                               ++toxic_count;
-                                                       }
-                                                       if (a.is_unaligned_eof()) {
-                                                               ++unaligned_eof_count;
-                                                       }
-                                               }
-                                               ++total_count;
-                                       }
-                                       ++occupancy.at(this_bucket_occupied_count * width / (1 + c_cells_per_bucket) );
-                                       // Count these instead of calculating the number so we get better stats in case of exceptions
-                                       occupied_count += this_bucket_occupied_count;
-                               }
-                               if (duplicate_bugs_found) {
-                                       set_extent_dirty_locked(ext);
+                               const auto &hist = m_extent_metadata.at(ext).m_fill_histogram;
+                               for (size_t fill = 0; fill < hist.size(); ++fill) {
+                                       const size_t buckets_at_fill = hist[fill];
+                                       occupancy.at(fill * width / (1 + c_cells_per_bucket)) += buckets_at_fill;
+                                       occupied_count += fill * buckets_at_fill;
                                }
                        });
                }
@@ -357,8 +347,6 @@ BeesHashTable::prefetch_loop()
                        out << "\n";
                }
 
-               size_t uncompressed_count = occupied_count - compressed_offset_count;
-
                ostringstream graph_blob;
 
                graph_blob << "Now:     " << format_time(time(NULL)) << "\n";
@@ -368,11 +356,7 @@ BeesHashTable::prefetch_loop()
 
                graph_blob
                        << "\nHash table page occupancy histogram (" << occupied_count << "/" << total_count << " cells occupied, " << (occupied_count * 100 / total_count) << "%)\n"
-                       << out.str() << "0%      |      25%      |      50%      |      75%      |   100% page fill\n"
-                       << "compressed " << compressed_count << " (" << percent(compressed_count, occupied_count) << ")\n"
-                       << "uncompressed " << uncompressed_count << " (" << percent(uncompressed_count, occupied_count) << ")"
-                       << " unaligned_eof " << unaligned_eof_count << " (" << percent(unaligned_eof_count, occupied_count) << ")"
-                       << " toxic " << toxic_count << " (" << percent(toxic_count, occupied_count) << ")";
+                       << out.str() << "0%      |      25%      |      50%      |      75%      |   100% page fill";
 
                graph_blob << "\n\n";
 
@@ -800,14 +784,18 @@ BeesHashTable::prepare()
        // faster to steady state and lets the lookup/insert paths assume the
        // table is always resident.  Memory-only mode skips this —
        // the pages are already zero from MAP_ANONYMOUS.
-       if (m_persistent) {
-               for (uint64_t ext = 0; ext < m_extents; ++ext) {
+       for (uint64_t ext = 0; ext < m_extents; ++ext) {
+               if (m_persistent) {
                        BEESNOTE("loading hash extent #" << ext << " of " << m_extents);
                        uint8_t *const extent_begin = m_extent_ptr[ext    ].p_byte;
                        uint8_t *const extent_end   = m_extent_ptr[ext + 1].p_byte;
                        pread_or_die(m_fd, extent_begin, extent_end - extent_begin, extent_begin - m_byte_ptr);
                        BEESCOUNT(hash_extent_in);
                }
+               // Seed the occupancy fragment from the just-loaded (or, in
+               // memory-only mode, zeroed) extent so the survey has data before
+               // the first writeback.
+               recompute_extent_fill_histogram_locked(ext);
        }
 
        // Pre-fault and lock now, while we know which size the host has to
index 69f63562ffd4b95594a1e865823b1e91ac12d822..a2531e6666b4543c6e51ace8ab8578e2fb8c21b3 100644 (file)
@@ -866,6 +866,12 @@ private:
        struct ExtentMetaData {
                shared_ptr<mutex> m_mutex_ptr;  ///< Serializes concurrent access to this extent.
                bool    m_dirty = false;         ///< True when the extent has unsaved modifications.
+               /// Per-bucket occupancy histogram for this extent: index = number of
+               /// occupied cells in a bucket, value = number of buckets at that
+               /// occupancy.  Refreshed under the extent lock at load and on each
+               /// writeback; summed by the periodic survey instead of walking cells.
+               /// uint8_t suffices: an extent holds only c_buckets_per_extent buckets.
+               array<uint8_t, c_cells_per_bucket + 1> m_fill_histogram {};
                ExtentMetaData();
        };
        vector<ExtentMetaData>  m_extent_metadata;
@@ -910,6 +916,7 @@ private:
 
        void writeback_loop();
        void prefetch_loop();
+       void recompute_extent_fill_histogram_locked(uint64_t extent_index);
        void try_mmap_flags(int flags);
        pair<Cell *, Cell *> get_cell_range(HashType hash);
        pair<uint8_t *, uint8_t *> get_extent_range(HashType hash);