example_render_log.rs

  1//! `RenderLog` — a diagnostic panel that records which components re-render
  2//! and when, letting you observe GPUI's caching behaviour in real time.
  3//!
  4//! Renders are grouped by frame (delimited by `begin_frame()` calls).
  5//! Consecutive frames with the same set of components are collapsed into
  6//! a single line with a repeat counter, so an animation that re-renders
  7//! `ExampleInput + ExampleEditor` 30 times shows as one line with `×30`.
  8
  9use std::time::Instant;
 10
 11use gpui::{App, Context, Entity, IntoViewElement, Window, div, hsla, prelude::*, px};
 12
 13// ---------------------------------------------------------------------------
 14// RenderLog entity
 15// ---------------------------------------------------------------------------
 16
 17pub struct RenderLog {
 18    current_frame: Vec<&'static str>,
 19    frames: Vec<RenderFrame>,
 20    start_time: Instant,
 21}
 22
 23struct RenderFrame {
 24    components: Vec<&'static str>,
 25    count: usize,
 26    last_timestamp: Instant,
 27}
 28
 29impl RenderLog {
 30    pub fn new(_cx: &mut Context<Self>) -> Self {
 31        Self {
 32            current_frame: Vec::new(),
 33            frames: Vec::new(),
 34            start_time: Instant::now(),
 35        }
 36    }
 37
 38    /// Mark the start of a new render frame. Finalizes the previous frame
 39    /// and either merges it with the last entry (if the same components
 40    /// rendered) or pushes a new entry.
 41    ///
 42    /// Call this at the top of the root view's `render()`, before any
 43    /// children have a chance to call `log()`.
 44    pub fn begin_frame(&mut self) {
 45        if self.current_frame.is_empty() {
 46            return;
 47        }
 48
 49        let mut components = std::mem::take(&mut self.current_frame);
 50        components.sort();
 51        components.dedup();
 52
 53        let now = Instant::now();
 54
 55        if let Some(last) = self.frames.last_mut() {
 56            if last.components == components {
 57                last.count += 1;
 58                last.last_timestamp = now;
 59                return;
 60            }
 61        }
 62
 63        self.frames.push(RenderFrame {
 64            components,
 65            count: 1,
 66            last_timestamp: now,
 67        });
 68
 69        if self.frames.len() > 50 {
 70            self.frames.drain(0..self.frames.len() - 50);
 71        }
 72    }
 73
 74    /// Record that `component` rendered in the current frame.
 75    /// Does **not** call `cx.notify()` — the panel updates passively when
 76    /// its parent re-renders, avoiding an infinite invalidation loop.
 77    pub fn log(&mut self, component: &'static str) {
 78        self.current_frame.push(component);
 79    }
 80
 81    #[cfg(test)]
 82    fn frame_count(&self) -> usize {
 83        self.frames.len()
 84    }
 85
 86    #[cfg(test)]
 87    fn frame_at(&self, index: usize) -> Option<(&[&'static str], usize)> {
 88        self.frames
 89            .get(index)
 90            .map(|f| (f.components.as_slice(), f.count))
 91    }
 92}
 93
 94// ---------------------------------------------------------------------------
 95// RenderLogPanel — stateless ComponentView that displays the log
 96// ---------------------------------------------------------------------------
 97
 98#[derive(Hash, IntoViewElement)]
 99pub struct RenderLogPanel {
100    log: Entity<RenderLog>,
101}
102
103impl RenderLogPanel {
104    pub fn new(log: Entity<RenderLog>) -> Self {
105        Self { log }
106    }
107}
108
109impl gpui::ComponentView for RenderLogPanel {
110    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
111        let log = self.log.read(cx);
112        let start = log.start_time;
113
114        div()
115            .flex()
116            .flex_col()
117            .gap(px(1.))
118            .p(px(8.))
119            .bg(hsla(0., 0., 0.12, 1.))
120            .rounded(px(4.))
121            .max_h(px(180.))
122            .overflow_hidden()
123            .child(
124                div()
125                    .text_xs()
126                    .text_color(hsla(0., 0., 0.55, 1.))
127                    .mb(px(4.))
128                    .child("Render log"),
129            )
130            .children(
131                log.frames
132                    .iter()
133                    .rev()
134                    .take(20)
135                    .collect::<Vec<_>>()
136                    .into_iter()
137                    .rev()
138                    .map(|frame| {
139                        let elapsed = frame.last_timestamp.duration_since(start);
140                        let secs = elapsed.as_secs_f64();
141                        let names = frame.components.join(", ");
142                        let count_str = if frame.count > 1 {
143                            format!(" ×{}", frame.count)
144                        } else {
145                            String::new()
146                        };
147
148                        div()
149                            .flex()
150                            .text_xs()
151                            .child(
152                                div()
153                                    .text_color(hsla(120. / 360., 0.7, 0.65, 1.))
154                                    .child(names),
155                            )
156                            .child(
157                                div()
158                                    .text_color(hsla(50. / 360., 0.8, 0.65, 1.))
159                                    .child(count_str),
160                            )
161                            .child(
162                                div()
163                                    .text_color(hsla(0., 0., 0.4, 1.))
164                                    .ml(px(8.))
165                                    .child(format!("+{:.1}s", secs)),
166                            )
167                    }),
168            )
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Tests
174// ---------------------------------------------------------------------------
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn new_log() -> RenderLog {
181        RenderLog {
182            current_frame: Vec::new(),
183            frames: Vec::new(),
184            start_time: Instant::now(),
185        }
186    }
187
188    #[test]
189    fn test_log_groups_by_frame() {
190        let mut log = new_log();
191
192        log.log("ExampleInput");
193        log.log("ExampleEditor");
194        log.begin_frame();
195
196        assert_eq!(log.frame_count(), 1);
197        assert_eq!(
198            log.frame_at(0),
199            Some((["ExampleEditor", "ExampleInput"].as_slice(), 1))
200        );
201    }
202
203    #[test]
204    fn test_consecutive_identical_frames_collapse() {
205        let mut log = new_log();
206
207        // Three identical frames: Input + Editor
208        log.log("ExampleInput");
209        log.log("ExampleEditor");
210        log.begin_frame();
211
212        log.log("ExampleInput");
213        log.log("ExampleEditor");
214        log.begin_frame();
215
216        log.log("ExampleEditor");
217        log.log("ExampleInput");
218        log.begin_frame();
219
220        // Should collapse to one entry with count 3
221        assert_eq!(log.frame_count(), 1);
222        assert_eq!(
223            log.frame_at(0),
224            Some((["ExampleEditor", "ExampleInput"].as_slice(), 3))
225        );
226    }
227
228    #[test]
229    fn test_different_frames_dont_collapse() {
230        let mut log = new_log();
231
232        log.log("ExampleInput");
233        log.log("ExampleEditor");
234        log.begin_frame();
235
236        log.log("EditorInfo");
237        log.begin_frame();
238
239        assert_eq!(log.frame_count(), 2);
240        assert_eq!(
241            log.frame_at(0),
242            Some((["ExampleEditor", "ExampleInput"].as_slice(), 1))
243        );
244        assert_eq!(log.frame_at(1), Some((["EditorInfo"].as_slice(), 1)));
245    }
246
247    #[test]
248    fn test_collapse_resumes_after_different_frame() {
249        let mut log = new_log();
250
251        // 2x Input+Editor, then 1x EditorInfo, then 3x Input+Editor
252        for _ in 0..2 {
253            log.log("ExampleInput");
254            log.log("ExampleEditor");
255            log.begin_frame();
256        }
257
258        log.log("EditorInfo");
259        log.begin_frame();
260
261        for _ in 0..3 {
262            log.log("ExampleInput");
263            log.log("ExampleEditor");
264            log.begin_frame();
265        }
266
267        assert_eq!(log.frame_count(), 3);
268        assert_eq!(log.frame_at(0).map(|(_, c)| c), Some(2));
269        assert_eq!(log.frame_at(1).map(|(_, c)| c), Some(1));
270        assert_eq!(log.frame_at(2).map(|(_, c)| c), Some(3));
271    }
272
273    #[test]
274    fn test_empty_frame_is_ignored() {
275        let mut log = new_log();
276
277        log.begin_frame();
278        assert_eq!(log.frame_count(), 0);
279
280        log.log("ExampleInput");
281        log.begin_frame();
282        assert_eq!(log.frame_count(), 1);
283
284        log.begin_frame();
285        assert_eq!(log.frame_count(), 1);
286    }
287
288    #[test]
289    fn test_duplicate_components_in_frame_are_deduped() {
290        let mut log = new_log();
291
292        log.log("ExampleInput");
293        log.log("ExampleInput");
294        log.log("ExampleEditor");
295        log.begin_frame();
296
297        assert_eq!(log.frame_count(), 1);
298        assert_eq!(
299            log.frame_at(0),
300            Some((["ExampleEditor", "ExampleInput"].as_slice(), 1))
301        );
302    }
303}