]> git.hungrycats.org Git - bees/commitdiff
heatmap: render heatmaps as per-interval deltas via snapshots
authorZygo Blaxell <bees@furryterror.org>
Mon, 22 Jun 2026 04:23:10 +0000 (00:23 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sun, 30 Aug 2026 23:48:46 +0000 (19:48 -0400)
Handle the heatmaps the way the delta-counts token handles event
counters instead: keep the live counters monotonic and diff snapshots.

BeesHeatmap holds atomic cells and cannot be copied, so add a detached,
copiable Snapshot (plain uint64_t grid) produced by snapshot(), with
count() and a cellwise operator-.  print() now takes a Snapshot; a thin
non-static print(os) convenience snapshots the live map and renders it.
The grid formatter is factored into a shared print_cells() used by both
paths.  reset() loses its only caller and is removed (the unit test's
reset case is replaced with a snapshot/delta case).

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

index 98cc4f2aa980ca44cecf1dce266045ec99fe2c2d..3a1e23d300b64502d96cca7baf4bdae01244fd1d 100644 (file)
@@ -10,17 +10,14 @@ using namespace crucible;
 using namespace std;
 
 ostream &
-BeesHeatmap::print(ostream &os) const
+BeesHeatmap::print_cells(ostream &os, const string &name,
+       const uint64_t cells[X_BUCKETS][T_BUCKETS])
 {
-       // Snapshot the populated bounds.
+       // Find the populated bounds.
        int x_lo = X_BUCKETS, x_hi = -1, t_lo = T_BUCKETS, t_hi = -1;
-       uint64_t cell[X_BUCKETS][T_BUCKETS];
        for (int xi = 0; xi < X_BUCKETS; ++xi) {
                for (int ti = 0; ti < T_BUCKETS; ++ti) {
-                       const uint64_t c =
-                               m_cells[xi][ti].load(memory_order_relaxed);
-                       cell[xi][ti] = c;
-                       if (c) {
+                       if (cells[xi][ti]) {
                                x_lo = min(x_lo, xi);
                                x_hi = max(x_hi, xi);
                                t_lo = min(t_lo, ti);
@@ -28,7 +25,7 @@ BeesHeatmap::print(ostream &os) const
                        }
                }
        }
-       os << "heatmap " << m_name;
+       os << "heatmap " << name;
        if (x_hi < 0) {
                os << ": (empty)\n";
                return os;
@@ -56,7 +53,7 @@ BeesHeatmap::print(ostream &os) const
                row.push_back(Table::Text(x_label(xi)));
                uint64_t row_total = 0;
                for (int ti = t_lo; ti <= t_hi; ++ti) {
-                       const uint64_t c = cell[xi][ti];
+                       const uint64_t c = cells[xi][ti];
                        row_total += c;
                        row.push_back(c ? Table::Number(c) : Table::Text("."));
                }
@@ -71,7 +68,7 @@ BeesHeatmap::print(ostream &os) const
        for (int ti = t_lo; ti <= t_hi; ++ti) {
                uint64_t col_total = 0;
                for (int xi = x_lo; xi <= x_hi; ++xi) {
-                       col_total += cell[xi][ti];
+                       col_total += cells[xi][ti];
                }
                grand += col_total;
                footer.push_back(Table::Number(col_total));
@@ -86,3 +83,48 @@ BeesHeatmap::print(ostream &os) const
        os << table;
        return os;
 }
+
+BeesHeatmap::Snapshot
+BeesHeatmap::snapshot() const
+{
+       Snapshot snap;
+       snap.m_name = m_name;
+       for (int xi = 0; xi < X_BUCKETS; ++xi) {
+               for (int ti = 0; ti < T_BUCKETS; ++ti) {
+                       snap.m_cells[xi][ti] =
+                               m_cells[xi][ti].load(memory_order_relaxed);
+               }
+       }
+       return snap;
+}
+
+ostream &
+BeesHeatmap::print(ostream &os, const Snapshot &snap)
+{
+       return print_cells(os, snap.m_name, snap.m_cells);
+}
+
+uint64_t
+BeesHeatmap::Snapshot::count() const
+{
+       uint64_t total = 0;
+       for (int xi = 0; xi < X_BUCKETS; ++xi) {
+               for (int ti = 0; ti < T_BUCKETS; ++ti) {
+                       total += m_cells[xi][ti];
+               }
+       }
+       return total;
+}
+
+BeesHeatmap::Snapshot
+BeesHeatmap::Snapshot::operator-(const Snapshot &older) const
+{
+       Snapshot diff;
+       diff.m_name = m_name;
+       for (int xi = 0; xi < X_BUCKETS; ++xi) {
+               for (int ti = 0; ti < T_BUCKETS; ++ti) {
+                       diff.m_cells[xi][ti] = m_cells[xi][ti] - older.m_cells[xi][ti];
+               }
+       }
+       return diff;
+}
index c6a9bced48c4bdd9789c3e3fe9d3429780d5d6da..c87b4c49a201e7370029881a644ff046388849ff 100644 (file)
 /// input-region count, both against the same elapsed time.  Only print()
 /// reaches into libcrucible (Table), and only for layout.
 class BeesHeatmap {
+       // Bucket geometry.  Declared first because Snapshot sizes its grid from
+       // these constants.
+       //
+       // Magnitude exponents 2^0 .. 2^X_EXP_MAX, plus a dedicated zero bucket.
+       // 2^32 is far more than we expect to count; the spare rows are a few KB
+       // and print() hides the unpopulated ones anyway.
+       static constexpr int X_EXP_MAX = 32;            // up to ~4 billion
+       static constexpr int X_BUCKETS = X_EXP_MAX + 2; // [0] = zero, [1..] = 2^(k-1)
+       // Time exponents 2^T_EXP_MIN .. 2^T_EXP_MAX seconds (~1ms .. ~12 days).
+       // The top end covers the longest Task lifetimes (which include queueing,
+       // restarts, and loadavg throttling, so a 20-hour wall time is normal);
+       // the bottom bucket absorbs every sub-millisecond plan.
+       static constexpr int T_EXP_MIN = -10;
+       static constexpr int T_EXP_MAX = 20;
+       static constexpr int T_BUCKETS = T_EXP_MAX - T_EXP_MIN + 1;
+
 public:
+       /// A detached, copiable value-copy of a heatmap's cells at one instant.
+       /// BeesHeatmap itself holds atomic counters and cannot be copied;
+       /// snapshot() reads them into one of these so a report can diff two
+       /// instants (and print the difference) without resetting the live
+       /// counters that the planner threads keep writing.
+       class Snapshot {
+               friend class BeesHeatmap;
+               std::string m_name;
+               uint64_t m_cells[X_BUCKETS][T_BUCKETS] {};
+       public:
+               /// Total number of events in this snapshot.
+               uint64_t count() const;
+               /// Cellwise difference (this - @p older).  Only ever applied to two
+               /// snapshots of the same monotonic counter, so cells never underflow.
+               Snapshot operator-(const Snapshot &older) const;
+       };
+
        explicit BeesHeatmap(std::string name) :
                m_name(std::move(name)) {}
 
@@ -40,18 +73,6 @@ public:
                        .fetch_add(1, std::memory_order_relaxed);
        }
 
-       /// Zero all buckets.  Safe to call concurrently with add()/print();
-       /// individual cells reset atomically, so a concurrent add() is either
-       /// counted or not, never corrupted.
-       void reset()
-       {
-               for (int xi = 0; xi < X_BUCKETS; ++xi) {
-                       for (int ti = 0; ti < T_BUCKETS; ++ti) {
-                               m_cells[xi][ti].store(0, std::memory_order_relaxed);
-                       }
-               }
-       }
-
        /// Total number of events recorded.
        uint64_t count() const
        {
@@ -66,24 +87,17 @@ public:
 
        const std::string &name() const { return m_name; }
 
-       /// Render the populated sub-grid (magnitude rows x time columns) with
-       /// row and column totals.  An empty heatmap prints a single line.
-       std::ostream &print(std::ostream &os) const;
+       /// Copy the live atomic cells into a detached snapshot.
+       Snapshot snapshot() const;
 
-private:
-       // Magnitude exponents 2^0 .. 2^X_EXP_MAX, plus a dedicated zero bucket.
-       // 2^32 is far more than we expect to count; the spare rows are a few KB
-       // and print() hides the unpopulated ones anyway.
-       static constexpr int X_EXP_MAX = 32;            // up to ~4 billion
-       static constexpr int X_BUCKETS = X_EXP_MAX + 2; // [0] = zero, [1..] = 2^(k-1)
-       // Time exponents 2^T_EXP_MIN .. 2^T_EXP_MAX seconds (~1ms .. ~12 days).
-       // The top end covers the longest Task lifetimes (which include queueing,
-       // restarts, and loadavg throttling, so a 20-hour wall time is normal);
-       // the bottom bucket absorbs every sub-millisecond plan.
-       static constexpr int T_EXP_MIN = -10;
-       static constexpr int T_EXP_MAX = 20;
-       static constexpr int T_BUCKETS = T_EXP_MAX - T_EXP_MIN + 1;
+       /// Render @p snap as a populated sub-grid (magnitude rows x time columns)
+       /// with row and column totals.  An empty snapshot prints a single line.
+       static std::ostream &print(std::ostream &os, const Snapshot &snap);
+
+       /// Convenience: snapshot the live heatmap and render it.
+       std::ostream &print(std::ostream &os) const { return print(os, snapshot()); }
 
+private:
        static int x_bucket(uint64_t x)
        {
                if (x == 0) {
@@ -147,6 +161,10 @@ private:
                return buf;
        }
 
+       // Shared grid formatter used by both print() entry points.
+       static std::ostream &print_cells(std::ostream &os, const std::string &name,
+               const uint64_t cells[X_BUCKETS][T_BUCKETS]);
+
        std::string m_name;
        std::atomic<uint64_t> m_cells[X_BUCKETS][T_BUCKETS] {};
 };