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}