perf: a frozen frame is not re-rendered sixty times a second

The tick queued a GL render unconditionally. In `Held` and `Paused` the rows
are bound to an immutable snapshot on a pinned axis, so a frame in which none
of the picture's own terms moved is the same frame again. The gate keys on a
coarse signature of what a frozen view can be re-framed by, opens for
`kSettleRenders` frames on a change because the figure lays out per render, and
has a floor that renders anyway every few ticks. The layout editor is excluded:
its buttons are placed from the last frame's rects, so a skipped frame there is
a stale widget rather than a stale picture. `skips=` in the probe dump is the
reading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-13 14:50:55 +02:00
1 parent 84c158ff21
commit be71a1d4bb
5 files changed
+130 -4

No files matched your search

+45 -1
View File
@@ -3706,7 +3706,51 @@ gboolean tick(gpointer user) {
window_w < kSidebarRoom);
}
}
if (app->area_widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(app->area_widget));
// The render, queued once a frame -- but not unconditionally. In `Held` and `Paused` the rows
// are bound to an immutable snapshot on a pinned axis and the producer is feeding the live ring,
// not the series on screen, so a frame in which none of the picture's own terms moved is the
// same frame again. Every other mode renders: `Live` and `Capturing` roll new samples under the
// axis, and `Waiting` moves the live epoch, so in all three the picture differs every frame.
//
// The gate is two parts, and the second is what makes the first safe to keep coarse:
// * a signature of what a frozen view *can* be re-framed by -- a new snapshot, a pan or zoom
// (the group's range, which is where the gestures and the pin both write), a row added or
// removed -- which renders the moment it changes;
// * `kStaticRenderEvery`, a floor that renders anyway every few ticks, so anything not in the
// signature is late rather than lost. A gate that can only be opened by a list of state is
// a gate that goes wrong the first time the list is one item short.
// Interaction needs neither: `PlotArea` queues its own render from the pointer, scroll and drag
// handlers, so a crosshair and a cursor drag are immediate whatever this decides.
//
// The layout editor is excluded outright: its buttons are placed from the rects the last frame
// was laid out with (`sync_layout_editor()` above), so it is the one state where a skipped frame
// is a stale *widget* and not just a stale picture.
if (app->area_widget != nullptr) {
const bool frozen = (display == DisplayMode::Held || display == DisplayMode::Paused) &&
!app->layout_editing;
const plot::Range view = app->rows->view_range();
const std::uint64_t seq = model.event_seq();
const int rows = app->rows->row_count();
const bool same = frozen && seq == app->render_event_seq && view.min == app->render_x_min &&
view.max == app->render_x_max && rows == app->render_rows;
// A change opens the gate for `kSettleRenders` frames, not one: the figure's left gutter is a
// monotone floor that converges over two layout passes (see the `layout over N frames`
// reading), and a layout pass happens per *render* -- so a single frame after a re-frame would
// leave the gutter one pass short until the floor below fired, 64 ms later.
if (!same) app->render_open = kSettleRenders;
if (app->render_open > 0 || app->render_skipped + 1 >= kStaticRenderEvery) {
if (app->render_open > 0) --app->render_open;
app->render_skipped = 0;
gtk_gl_area_queue_render(GTK_GL_AREA(app->area_widget));
} else {
++app->render_skipped;
++app->dbg_render_skips;
}
app->render_event_seq = seq;
app->render_x_min = view.min;
app->render_x_max = view.max;
app->render_rows = rows;
}
// What the side column *demands*, not what it got. A GTK box's minimum width is its
// children's, a `GtkLabel`'s minimum is its text, and a minimum larger than the screen is
// honoured by growing the toplevel -- which hands the plot an allocation wider than the window