fix: a paused view carries its own LOD pyramid, so it is drawn like a held event

publish_pause_view() filled stamps, values and y and left pyramids empty, so
level_for() descended to raw samples and a frozen pause was re-gathered from
every sample it held on every frame. It cost draw time (12.34 -> 18.18 ms mean)
and, because stage_trace_density() normalises each column against its own peak,
it also changed how the trace looked -- which is how it was noticed.

Both publishes are now timed and reported as `publish cost:`, counted apart
because they stall opposite threads: the event publish runs on the producer
under mutex() and blocks the UI, the pause publish runs on the UI thread and
blocks the producer. That is the number a second live ring has to be judged
against.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-06 20:36:27 +02:00
1 parent 13574bfcef
commit d2c51f2448
5 files changed
+175 -4

No files matched your search

+47
View File
@@ -224,6 +224,37 @@ class ScopeModel {
std::uint64_t math_cost_ns() const noexcept {
return math_cost_ns_.load(std::memory_order_relaxed);
}
/// What freezing a view costs, and *which thread it costs it to*. The two publishes are stalls on
/// opposite sides of `mutex()` and so are counted apart: `publish_event()` runs on the **producer**
/// with the lock already held, so its cost lands on the UI as a long tick (the documented `tick`
/// max); `publish_pause_view()` runs on the **UI** thread and takes the lock, so its cost lands on
/// the producer and can only surface as `gaps`. Neither is attributable in a frame mean, which is
/// why they are counted rather than inferred. Each counter is written by one thread and read by the
/// dump, so a relaxed store of a value already in hand is enough.
///
/// This is the number to judge a second live ring against: freezing a whole `BufferStore` would
/// make both figures zero, since a `ChannelBuffer` maintains its pyramid incrementally and there
/// would be nothing to copy -- but it costs a second budget's worth of memory and a second
/// per-sample write on the producer, so it is worth paying only if the worst figures here are
/// large enough to matter.
std::uint64_t publish_event_count() const noexcept {
return publish_event_count_.load(std::memory_order_relaxed);
}
std::uint64_t publish_event_us_total() const noexcept {
return publish_event_us_total_.load(std::memory_order_relaxed);
}
std::uint64_t publish_event_us_worst() const noexcept {
return publish_event_us_worst_.load(std::memory_order_relaxed);
}
std::uint64_t publish_pause_count() const noexcept {
return publish_pause_count_.load(std::memory_order_relaxed);
}
std::uint64_t publish_pause_us_total() const noexcept {
return publish_pause_us_total_.load(std::memory_order_relaxed);
}
std::uint64_t publish_pause_us_worst() const noexcept {
return publish_pause_us_worst_.load(std::memory_order_relaxed);
}
const std::vector<SignalInfo>& signals() const noexcept { return signals_; }
const std::vector<ChannelInfo>& channels() const noexcept { return channels_; }
@@ -491,6 +522,22 @@ class ScopeModel {
std::atomic<std::uint64_t> math_slots_{0};
std::atomic<std::uint64_t> math_timed_{0};
std::atomic<std::uint64_t> math_cost_ns_{0};
/// See `publish_event_count()`. Single-writer each: the first three are the producer's, the last
/// three the UI thread's.
std::atomic<std::uint64_t> publish_event_count_{0};
std::atomic<std::uint64_t> publish_event_us_total_{0};
std::atomic<std::uint64_t> publish_event_us_worst_{0};
std::atomic<std::uint64_t> publish_pause_count_{0};
std::atomic<std::uint64_t> publish_pause_us_total_{0};
std::atomic<std::uint64_t> publish_pause_us_worst_{0};
/// Fold one publish's elapsed microseconds into a counter triple. Called by the one thread that
/// owns the triple, so the load/store pairs need no read-modify-write.
static void note_publish(std::atomic<std::uint64_t>& count, std::atomic<std::uint64_t>& total,
std::atomic<std::uint64_t>& worst, std::uint64_t us) noexcept {
count.store(count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed);
total.store(total.load(std::memory_order_relaxed) + us, std::memory_order_relaxed);
if (us > worst.load(std::memory_order_relaxed)) worst.store(us, std::memory_order_relaxed);
}
std::vector<bool> enabled_;
Partition part_;