Skip to content

Commit ce8ba41

Browse files
authored
Merge pull request #307 from charliek/feature/plan-018-gtk-dirty-rows
Engine track E4 no-go + E3b: GTK dirty-row rebuild and real render_stats
2 parents 9032f46 + 85e6644 commit ce8ba41

10 files changed

Lines changed: 1106 additions & 178 deletions

File tree

crates/roost-iced/src/perf.rs

Lines changed: 13 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -26,95 +26,38 @@
2626
//! tests don't construct. There is no per-tab equivalent of them for the
2727
//! same reason — see [`TabRenderStats`].
2828
29-
use std::sync::atomic::{AtomicU64, Ordering};
3029
use std::time::Duration;
3130

32-
static REFRESH_CALLS: AtomicU64 = AtomicU64::new(0);
33-
static REFRESH_NANOS: AtomicU64 = AtomicU64::new(0);
34-
static ROWS_REBUILT: AtomicU64 = AtomicU64::new(0);
35-
static CELLS_WALKED: AtomicU64 = AtomicU64::new(0);
36-
static DRAW_CALLS: AtomicU64 = AtomicU64::new(0);
37-
static DRAW_NANOS: AtomicU64 = AtomicU64::new(0);
38-
static FILL_TEXT_CALLS: AtomicU64 = AtomicU64::new(0);
31+
/// `RenderStats` + `TabRenderStats` + the atomic aggregate machinery moved
32+
/// to `roost-ui-model::render_stats` once GTK needed the identical shape
33+
/// (plan 018 D4); re-exported here so every call site in this crate keeps
34+
/// compiling unchanged.
35+
pub use roost_ui_model::render_stats::{RenderStats, TabRenderStats};
3936

40-
/// A read of the process-global aggregate at one instant. Every field is a
41-
/// running total since process start (or the last [`reset`]).
42-
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43-
pub struct RenderStats {
44-
pub refresh_calls: u64,
45-
pub refresh_nanos: u64,
46-
pub rows_rebuilt: u64,
47-
pub cells_walked: u64,
48-
pub draw_calls: u64,
49-
pub draw_nanos: u64,
50-
pub fill_text_calls: u64,
51-
}
52-
53-
/// Per-tab counters, folded into the global aggregate on every refresh.
54-
/// There is deliberately no per-tab equivalent of `draw_calls` /
55-
/// `draw_nanos` / `fill_text_calls`: `TerminalWidget::draw` renders from a
56-
/// `TerminalSnapshot` clone handed to it by iced and has no way back to the
57-
/// `TerminalTab` that produced it, so those three counters only exist in
58-
/// the global aggregate.
59-
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60-
pub struct TabRenderStats {
61-
pub refresh_calls: u64,
62-
pub refresh_nanos: u64,
63-
pub rows_rebuilt: u64,
64-
pub cells_walked: u64,
65-
}
37+
use roost_ui_model::render_stats::RenderStatsAggregate;
6638

67-
impl TabRenderStats {
68-
pub(crate) fn record_refresh(
69-
&mut self,
70-
elapsed: Duration,
71-
rows_rebuilt: u64,
72-
cells_walked: u64,
73-
) {
74-
self.refresh_calls += 1;
75-
self.refresh_nanos += elapsed.as_nanos() as u64;
76-
self.rows_rebuilt += rows_rebuilt;
77-
self.cells_walked += cells_walked;
78-
}
79-
}
39+
/// This UI's process-global aggregate. GTK holds its own in
40+
/// `roost-linux::perf`; the two never mix.
41+
static AGGREGATE: RenderStatsAggregate = RenderStatsAggregate::new();
8042

8143
/// Read the global aggregate. Backs the `app.render_stats` IPC op.
8244
pub fn snapshot() -> RenderStats {
83-
RenderStats {
84-
refresh_calls: REFRESH_CALLS.load(Ordering::Relaxed),
85-
refresh_nanos: REFRESH_NANOS.load(Ordering::Relaxed),
86-
rows_rebuilt: ROWS_REBUILT.load(Ordering::Relaxed),
87-
cells_walked: CELLS_WALKED.load(Ordering::Relaxed),
88-
draw_calls: DRAW_CALLS.load(Ordering::Relaxed),
89-
draw_nanos: DRAW_NANOS.load(Ordering::Relaxed),
90-
fill_text_calls: FILL_TEXT_CALLS.load(Ordering::Relaxed),
91-
}
45+
AGGREGATE.snapshot()
9246
}
9347

9448
/// Zero the global aggregate. Useful before an operation known to skew it
9549
/// (see the screenshot trap above) so the next read is uncontaminated.
9650
/// Exposed over IPC as `app.render_stats` with `reset: true`.
9751
pub fn reset() {
98-
REFRESH_CALLS.store(0, Ordering::Relaxed);
99-
REFRESH_NANOS.store(0, Ordering::Relaxed);
100-
ROWS_REBUILT.store(0, Ordering::Relaxed);
101-
CELLS_WALKED.store(0, Ordering::Relaxed);
102-
DRAW_CALLS.store(0, Ordering::Relaxed);
103-
DRAW_NANOS.store(0, Ordering::Relaxed);
104-
FILL_TEXT_CALLS.store(0, Ordering::Relaxed);
52+
AGGREGATE.reset();
10553
}
10654

10755
/// Fold one `refresh_snapshot` call into the global aggregate.
10856
pub(crate) fn record_refresh(elapsed: Duration, rows_rebuilt: u64, cells_walked: u64) {
109-
REFRESH_CALLS.fetch_add(1, Ordering::Relaxed);
110-
REFRESH_NANOS.fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed);
111-
ROWS_REBUILT.fetch_add(rows_rebuilt, Ordering::Relaxed);
112-
CELLS_WALKED.fetch_add(cells_walked, Ordering::Relaxed);
57+
AGGREGATE.record_refresh(elapsed, rows_rebuilt, cells_walked);
11358
}
11459

11560
/// Fold one `TerminalWidget::draw` call into the global aggregate.
11661
pub(crate) fn record_draw(elapsed: Duration, fill_text_calls: u64) {
117-
DRAW_CALLS.fetch_add(1, Ordering::Relaxed);
118-
DRAW_NANOS.fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed);
119-
FILL_TEXT_CALLS.fetch_add(fill_text_calls, Ordering::Relaxed);
62+
AGGREGATE.record_draw(elapsed, fill_text_calls);
12063
}

crates/roost-linux/src/app.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,15 +1099,20 @@ impl App {
10991099
UiRequest::WindowMetrics { reply } => {
11001100
let _ = reply.send(app.ipc_window_metrics());
11011101
}
1102-
UiRequest::AppRenderStats { reset: _, reply } => {
1103-
// Deliberate placeholder: the GTK renderer has no
1104-
// perf instrumentation yet (roadmap slice E3b).
1105-
// Zeros keep the op's contract identical on both
1106-
// Rust UIs — refusing here would make this the
1107-
// first op one Rust UI answers and the other
1108-
// doesn't, and there is no `unsupported` error
1109-
// code to refuse with.
1110-
let _ = reply.send(Ok(AppRenderStatsResult::default()));
1102+
UiRequest::AppRenderStats { reset, reply } => {
1103+
let stats = crate::perf::snapshot();
1104+
if reset {
1105+
crate::perf::reset();
1106+
}
1107+
let _ = reply.send(Ok(AppRenderStatsResult {
1108+
refresh_calls: stats.refresh_calls as i64,
1109+
refresh_nanos: stats.refresh_nanos as i64,
1110+
rows_rebuilt: stats.rows_rebuilt as i64,
1111+
cells_walked: stats.cells_walked as i64,
1112+
draw_calls: stats.draw_calls as i64,
1113+
draw_nanos: stats.draw_nanos as i64,
1114+
fill_text_calls: stats.fill_text_calls as i64,
1115+
}));
11111116
}
11121117
UiRequest::SidebarDump { reply } => {
11131118
let _ = reply.send(app.ipc_sidebar_dump());

crates/roost-linux/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod notification_inbox;
2626
mod palette;
2727
mod palette_ui;
2828
mod paste_image;
29+
mod perf;
2930
mod provider;
3031
mod rollup;
3132
mod sprite;

crates/roost-linux/src/perf.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! GTK render performance instrumentation. Counters and timings only —
2+
//! the shapes and the atomic machinery live in
3+
//! [`roost_ui_model::render_stats`]; this module is just this UI's
4+
//! process-global instance plus thin shims. `roost-iced` has the exact
5+
//! same shape over its own aggregate; the two never mix.
6+
//!
7+
//! GTK counter semantics (they differ from iced's, so read this before
8+
//! comparing numbers across the two UIs):
9+
//!
10+
//! - GTK's `paint` is refresh *and* draw in one pass. The seam is
11+
//! `TerminalViewState::refresh_cache` (renderer-free: `update` +
12+
//! dirty-row walk + counters, no Cairo) — `refresh_*` counts that phase,
13+
//! `draw_*` counts the Cairo phase that consumes its output. One
14+
//! `paint` therefore folds in exactly one refresh and one draw, unlike
15+
//! iced where a refresh (on PTY output) and a draw (on window redraw)
16+
//! are independently scheduled.
17+
//! - `fill_text_calls` counts `pango_cairo::show_layout` calls **plus**
18+
//! sprite draws, because a sprite *replaces* a glyph draw — the number
19+
//! means "glyph draws the pass emitted". iced has no sprite path
20+
//! (roadmap E5), so this field is not apples-to-apples across UIs.
21+
//! - `rows_rebuilt` / `cells_walked` mean the same as iced: rows visited
22+
//! by the walk and cells handed to the per-cell callback.
23+
//!
24+
//! Per-tab counters live on `TerminalViewState::render_stats` (a
25+
//! non-atomic [`roost_ui_model::render_stats::TabRenderStats`]); the
26+
//! aggregate here is what the `app.render_stats` IPC op reads.
27+
28+
use std::time::Duration;
29+
30+
use roost_ui_model::render_stats::{RenderStats, RenderStatsAggregate};
31+
32+
static AGGREGATE: RenderStatsAggregate = RenderStatsAggregate::new();
33+
34+
/// Read the global aggregate. Backs the `app.render_stats` IPC op.
35+
pub(crate) fn snapshot() -> RenderStats {
36+
AGGREGATE.snapshot()
37+
}
38+
39+
/// Zero the global aggregate. Exposed over IPC as `app.render_stats`
40+
/// with `reset: true`.
41+
pub(crate) fn reset() {
42+
AGGREGATE.reset();
43+
}
44+
45+
/// Fold one `refresh_cache` call into the global aggregate.
46+
pub(crate) fn record_refresh(elapsed: Duration, rows_rebuilt: u64, cells_walked: u64) {
47+
AGGREGATE.record_refresh(elapsed, rows_rebuilt, cells_walked);
48+
}
49+
50+
/// Fold one `paint` Cairo phase into the global aggregate.
51+
pub(crate) fn record_draw(elapsed: Duration, fill_text_calls: u64) {
52+
AGGREGATE.record_draw(elapsed, fill_text_calls);
53+
}

0 commit comments

Comments
 (0)