]> git.hungrycats.org Git - bees/commitdiff
reporter: render heatmaps as per-interval deltas via snapshots
authorZygo Blaxell <bees@furryterror.org>
Fri, 19 Jun 2026 17:27:40 +0000 (13:27 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:04:12 +0000 (00:04 -0400)
The heatmap report token used to zero the live planner heatmaps on its
own hourly timer so the picture tracked recent activity.  Resetting the
counters is the only writer that races the lock-free planner adds, and
it means any other consumer of the same heatmap sees a disturbed,
non-monotonic counter.

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).

The token keeps the previous snapshot of each heatmap and prints the
difference on every render, never touching the live counters, so the
window tracks the owning report's own interval.

Assisted-by: Claude-Code:claude-opus-4-8
docs/config-file.md
src/bees-reporter.cc
test/test-bees-heatmap.cc

index 0088d97d9fc5bca8a5b0cb8172817da14082e8fe..a884d216bfc089478fa01850007f42cb8c49e050 100644 (file)
@@ -601,7 +601,13 @@ not empty.
   * `hashtable` — hash table page-occupancy histogram.
   * `delta-counts` — event-counter change since this report last rendered.
   * `delta-rates` — that change divided by the elapsed time.
-  * `heatmap` — planner cost heatmaps (zeroed hourly when rendered).
+  * `heatmap` — planner cost heatmaps, showing the change since this report
+    last rendered.  Like `delta-counts`, the token snapshots the live
+    (monotonic) heatmap counters and prints the difference from its previous
+    snapshot, so the picture tracks the report's own interval without ever
+    resetting the counters the planner threads keep accumulating.  Each report
+    owns its own snapshot, so two reports rendering `heatmap` at different
+    intervals each see the change over their own interval.
 
 ### Built-in reports
 
index 76aadbaa9a80db0e3566640e41fee207aab7d241..a321c4fc3d187c1e5f7a8467e3e7e995ddbd9ba3 100644 (file)
@@ -65,27 +65,38 @@ public:
        }
 };
 
-/// Renders the planner cost heatmaps and zeroes them hourly so the picture
-/// reflects recent activity rather than all-time totals.  Planner threads
-/// accumulate the heatmaps lock-free; this token only reads and resets them.
-/// The hourly reset timer lives here, tying the reset to this report's
-/// cadence.
+/// Renders the change in the planner cost heatmaps since this token last
+/// rendered, the same way the delta-counts token handles event counters: it
+/// snapshots the live (monotonic) heatmaps and prints the difference from the
+/// previous snapshot, never resetting the counters the planner threads keep
+/// writing lock-free.  Each report owns its own token, so the delta spans
+/// that report's own interval.
 class HeatmapToken : public BeesReportToken {
        shared_ptr<BeesContext> m_ctx;
-       Timer m_reset_timer;
+       BeesHeatmap::Snapshot m_last_regions;
+       BeesHeatmap::Snapshot m_last_boundaries;
+       Timer m_timer;
 public:
-       explicit HeatmapToken(shared_ptr<BeesContext> ctx) : m_ctx(std::move(ctx)) {}
+       explicit HeatmapToken(shared_ptr<BeesContext> ctx) :
+               m_ctx(std::move(ctx)),
+               m_last_regions(m_ctx->plan_regions_heatmap().snapshot()),
+               m_last_boundaries(m_ctx->plan_boundaries_heatmap().snapshot())
+       {
+       }
        void render(ostream &os) override {
-               auto &regions = m_ctx->plan_regions_heatmap();
-               auto &boundaries = m_ctx->plan_boundaries_heatmap();
-               if (regions.count() > 0) {
-                       os << "PLANNER HEATMAPS (last " << m_reset_timer.age() << "s):\n";
-                       regions.print(os);
-                       boundaries.print(os);
-               }
-               if (m_reset_timer.age() >= 3600.0) {
-                       m_reset_timer.reset();
+               const auto regions = m_ctx->plan_regions_heatmap().snapshot();
+               const auto boundaries = m_ctx->plan_boundaries_heatmap().snapshot();
+               const auto age = m_timer.age();
+               const auto d_regions = regions - m_last_regions;
+               const auto d_boundaries = boundaries - m_last_boundaries;
+               if (d_regions.count() > 0) {
+                       os << "PLANNER HEATMAPS (last " << age << "s):\n";
+                       BeesHeatmap::print(os, d_regions);
+                       BeesHeatmap::print(os, d_boundaries);
                }
+               m_last_regions = regions;
+               m_last_boundaries = boundaries;
+               m_timer.lap();
        }
 };
 
index 7238ffe6d6ab7c09c48fc57e7c734ab236494155..71c6a56fa8bff4dd899f2938a039782a31ebd7cb 100644 (file)
@@ -57,6 +57,37 @@ test_bucket_sharing()
        assert(out.find("col_total") != string::npos);
 }
 
+static void
+test_snapshot_delta()
+{
+       BeesHeatmap hm("delta");
+       hm.add(3, 0.01);
+       const auto base = hm.snapshot();
+       assert(base.count() == 1);
+
+       hm.add(5, 0.02);
+       hm.add(5, 0.02);
+       const auto cur = hm.snapshot();
+       assert(cur.count() == 3);
+       // Snapshotting never touches the live counter: it stays monotonic.
+       assert(hm.count() == 3);
+
+       // The delta sees only the events recorded since the base snapshot.
+       const auto delta = cur - base;
+       assert(delta.count() == 2);
+
+       // A snapshot renders through the static print(os, snap) entry point.
+       ostringstream oss;
+       BeesHeatmap::print(oss, delta);
+       assert(oss.str().find("heatmap delta") != string::npos);
+
+       // An empty delta (no change between two equal snapshots) prints the
+       // single empty line, never a grid.
+       ostringstream oss_empty;
+       BeesHeatmap::print(oss_empty, cur - cur);
+       assert(oss_empty.str().find("(empty)") != string::npos);
+}
+
 static void
 test_clamping()
 {
@@ -75,6 +106,7 @@ main(int, char **)
        RUN_A_TEST(test_empty());
        RUN_A_TEST(test_counting());
        RUN_A_TEST(test_bucket_sharing());
+       RUN_A_TEST(test_snapshot_delta());
        RUN_A_TEST(test_clamping());
        return 0;
 }