1use crate::ir::{IrBatch, IrCommand};
11use crate::layout::{ComputedLayout, LayoutEngine};
12use crate::tree::{NodeId, ShadowTree};
13use crate::platform::PropValue;
14use serde::{Serialize, Deserialize};
15use std::collections::HashMap;
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct NodeSnapshot {
25 pub id: u64,
26 pub view_type: String,
27 pub component_name: Option<String>,
28 pub props: HashMap<String, serde_json::Value>,
29 pub children: Vec<NodeSnapshot>,
30 pub layout: Option<LayoutRect>,
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
35pub struct LayoutRect {
36 pub x: f32,
37 pub y: f32,
38 pub width: f32,
39 pub height: f32,
40}
41
42impl From<ComputedLayout> for LayoutRect {
43 fn from(cl: ComputedLayout) -> Self {
44 Self { x: cl.x, y: cl.y, width: cl.width, height: cl.height }
45 }
46}
47
48pub fn snapshot_tree(tree: &ShadowTree, layout_engine: &LayoutEngine) -> Option<NodeSnapshot> {
50 let root_id = tree.root()?;
51 Some(snapshot_node(root_id, tree, layout_engine))
52}
53
54fn snapshot_node(id: NodeId, tree: &ShadowTree, layout: &LayoutEngine) -> NodeSnapshot {
55 let node = tree.get(id).expect("node must exist");
56
57 let props: HashMap<String, serde_json::Value> = node.props.iter()
58 .map(|(k, v)| (k.clone(), prop_value_to_json(v)))
59 .collect();
60
61 let children: Vec<NodeSnapshot> = node.children.iter()
62 .map(|&child_id| snapshot_node(child_id, tree, layout))
63 .collect();
64
65 let layout_rect = layout.get_computed(id).map(|cl| LayoutRect::from(*cl));
66
67 NodeSnapshot {
68 id: id.0,
69 view_type: format!("{:?}", node.view_type),
70 component_name: node.component_name.clone(),
71 props,
72 children,
73 layout: layout_rect,
74 }
75}
76
77fn prop_value_to_json(v: &PropValue) -> serde_json::Value {
78 match v {
79 PropValue::String(s) => serde_json::Value::String(s.clone()),
80 PropValue::Bool(b) => serde_json::Value::Bool(*b),
81 PropValue::I32(i) => serde_json::json!(*i),
82 PropValue::F32(f) => serde_json::json!(*f),
83 PropValue::F64(f) => serde_json::json!(*f),
84 PropValue::Rect { x, y, width, height } => serde_json::json!({
85 "x": x, "y": y, "width": width, "height": height
86 }),
87 PropValue::Color(c) => serde_json::json!({
88 "r": c.r, "g": c.g, "b": c.b, "a": c.a
89 }),
90 PropValue::Null => serde_json::Value::Null,
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct LayoutOverlay {
101 pub rects: Vec<OverlayRect>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct OverlayRect {
107 pub node_id: u64,
108 pub view_type: String,
109 pub rect: LayoutRect,
110 pub is_highlighted: bool,
111}
112
113pub fn gather_overlay(
116 tree: &ShadowTree,
117 layout_engine: &LayoutEngine,
118 highlight_id: Option<NodeId>,
119) -> LayoutOverlay {
120 let mut rects = Vec::new();
121
122 for (&node_id, node) in tree.iter() {
123 if let Some(cl) = layout_engine.get_computed(node_id) {
124 rects.push(OverlayRect {
125 node_id: node_id.0,
126 view_type: format!("{:?}", node.view_type),
127 rect: LayoutRect::from(*cl),
128 is_highlighted: highlight_id == Some(node_id),
129 });
130 }
131 }
132
133 rects.sort_by_key(|r| r.node_id);
135 LayoutOverlay { rects }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct FrameRecord {
145 pub frame_number: u64,
146 pub total_ms: f64,
147 pub layout_ms: f64,
148 pub commit_count: u32,
149 pub node_count: u32,
150}
151
152pub struct FrameTimer {
154 start: Instant,
155 layout_duration: Duration,
156 commit_count: u32,
157}
158
159impl FrameTimer {
160 pub fn start() -> Self {
161 Self {
162 start: Instant::now(),
163 layout_duration: Duration::ZERO,
164 commit_count: 0,
165 }
166 }
167
168 pub fn record_layout(&mut self, duration: Duration) {
169 self.layout_duration += duration;
170 }
171
172 pub fn record_commit(&mut self) {
173 self.commit_count += 1;
174 }
175
176 pub fn finish(self, frame_number: u64, node_count: u32) -> FrameRecord {
177 let total = self.start.elapsed();
178 FrameRecord {
179 frame_number,
180 total_ms: total.as_secs_f64() * 1000.0,
181 layout_ms: self.layout_duration.as_secs_f64() * 1000.0,
182 commit_count: self.commit_count,
183 node_count,
184 }
185 }
186}
187
188pub struct Profiler {
190 frames: Vec<FrameRecord>,
191 max_frames: usize,
192 total_frames: u64,
193}
194
195impl Profiler {
196 pub fn new(max_frames: usize) -> Self {
197 Self {
198 frames: Vec::with_capacity(max_frames),
199 max_frames,
200 total_frames: 0,
201 }
202 }
203
204 pub fn push_frame(&mut self, record: FrameRecord) {
205 if self.frames.len() >= self.max_frames {
206 self.frames.remove(0);
207 }
208 self.frames.push(record);
209 self.total_frames += 1;
210 }
211
212 pub fn frames(&self) -> &[FrameRecord] {
213 &self.frames
214 }
215
216 pub fn total_frames(&self) -> u64 {
217 self.total_frames
218 }
219
220 pub fn summary(&self) -> ProfileSummary {
222 if self.frames.is_empty() {
223 return ProfileSummary::default();
224 }
225
226 let n = self.frames.len() as f64;
227 let avg_total = self.frames.iter().map(|f| f.total_ms).sum::<f64>() / n;
228 let avg_layout = self.frames.iter().map(|f| f.layout_ms).sum::<f64>() / n;
229 let max_total = self.frames.iter().map(|f| f.total_ms).fold(0.0_f64, f64::max);
230 let total_commits: u32 = self.frames.iter().map(|f| f.commit_count).sum();
231
232 ProfileSummary {
233 frame_count: self.frames.len() as u64,
234 avg_frame_ms: avg_total,
235 avg_layout_ms: avg_layout,
236 max_frame_ms: max_total,
237 total_commits,
238 }
239 }
240}
241
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct ProfileSummary {
245 pub frame_count: u64,
246 pub avg_frame_ms: f64,
247 pub avg_layout_ms: f64,
248 pub max_frame_ms: f64,
249 pub total_commits: u32,
250}
251
252pub struct IrRecorder {
258 batches: Vec<TimestampedBatch>,
259 recording: bool,
260 start_time: Option<Instant>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct TimestampedBatch {
266 pub offset_ms: f64,
267 pub batch: IrBatch,
268}
269
270impl IrRecorder {
271 pub fn new() -> Self {
272 Self {
273 batches: Vec::new(),
274 recording: false,
275 start_time: None,
276 }
277 }
278
279 pub fn start_recording(&mut self) {
280 self.batches.clear();
281 self.recording = true;
282 self.start_time = Some(Instant::now());
283 }
284
285 pub fn stop_recording(&mut self) {
286 self.recording = false;
287 }
288
289 pub fn is_recording(&self) -> bool {
290 self.recording
291 }
292
293 pub fn record(&mut self, batch: &IrBatch) {
295 if !self.recording { return; }
296 let offset_ms = self.start_time
297 .map(|t| t.elapsed().as_secs_f64() * 1000.0)
298 .unwrap_or(0.0);
299 self.batches.push(TimestampedBatch {
300 offset_ms,
301 batch: batch.clone(),
302 });
303 }
304
305 pub fn batches(&self) -> &[TimestampedBatch] {
307 &self.batches
308 }
309
310 pub fn len(&self) -> usize {
312 self.batches.len()
313 }
314
315 pub fn is_empty(&self) -> bool {
316 self.batches.is_empty()
317 }
318
319 pub fn export_json(&self) -> serde_json::Value {
321 serde_json::json!({
322 "version": 1,
323 "batch_count": self.batches.len(),
324 "batches": self.batches,
325 })
326 }
327}
328
329impl Default for IrRecorder {
330 fn default() -> Self { Self::new() }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(tag = "type")]
340pub enum DevToolsRequest {
341 #[serde(rename = "getTree")]
343 GetTree,
344
345 #[serde(rename = "getOverlay")]
347 GetOverlay { highlight_node_id: Option<u64> },
348
349 #[serde(rename = "getProfileSummary")]
351 GetProfileSummary,
352
353 #[serde(rename = "getFrames")]
355 GetFrames { count: Option<usize> },
356
357 #[serde(rename = "setRecording")]
359 SetRecording { enabled: bool },
360
361 #[serde(rename = "getRecording")]
363 GetRecording,
364
365 #[serde(rename = "highlightNode")]
367 HighlightNode { node_id: u64 },
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372#[serde(tag = "type")]
373pub enum DevToolsResponse {
374 #[serde(rename = "tree")]
375 Tree { root: Option<NodeSnapshot> },
376
377 #[serde(rename = "overlay")]
378 Overlay { overlay: LayoutOverlay },
379
380 #[serde(rename = "profileSummary")]
381 ProfileSummary { summary: ProfileSummary },
382
383 #[serde(rename = "frames")]
384 Frames { frames: Vec<FrameRecord> },
385
386 #[serde(rename = "recording")]
387 Recording { data: serde_json::Value },
388
389 #[serde(rename = "error")]
390 Error { message: String },
391}
392
393pub fn handle_devtools_request(
395 request: &DevToolsRequest,
396 tree: &ShadowTree,
397 layout_engine: &LayoutEngine,
398 profiler: &Profiler,
399 recorder: &mut IrRecorder,
400) -> DevToolsResponse {
401 match request {
402 DevToolsRequest::GetTree => {
403 let root = snapshot_tree(tree, layout_engine);
404 DevToolsResponse::Tree { root }
405 }
406 DevToolsRequest::GetOverlay { highlight_node_id } => {
407 let highlight = highlight_node_id.map(NodeId);
408 let overlay = gather_overlay(tree, layout_engine, highlight);
409 DevToolsResponse::Overlay { overlay }
410 }
411 DevToolsRequest::GetProfileSummary => {
412 let summary = profiler.summary();
413 DevToolsResponse::ProfileSummary { summary }
414 }
415 DevToolsRequest::GetFrames { count } => {
416 let all = profiler.frames();
417 let frames = match count {
418 Some(n) => all[all.len().saturating_sub(*n)..].to_vec(),
419 None => all.to_vec(),
420 };
421 DevToolsResponse::Frames { frames }
422 }
423 DevToolsRequest::SetRecording { enabled } => {
424 if *enabled {
425 recorder.start_recording();
426 } else {
427 recorder.stop_recording();
428 }
429 DevToolsResponse::Recording { data: serde_json::json!({"recording": enabled}) }
430 }
431 DevToolsRequest::GetRecording => {
432 let data = recorder.export_json();
433 DevToolsResponse::Recording { data }
434 }
435 DevToolsRequest::HighlightNode { node_id } => {
436 let highlight = Some(NodeId(*node_id));
437 let overlay = gather_overlay(tree, layout_engine, highlight);
438 DevToolsResponse::Overlay { overlay }
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::platform::ViewType;
447 use std::thread;
448
449 #[test]
450 fn test_snapshot_empty_tree() {
451 let tree = ShadowTree::new();
452 let layout = LayoutEngine::new();
453 assert!(snapshot_tree(&tree, &layout).is_none());
454 }
455
456 #[test]
457 fn test_snapshot_tree_with_nodes() {
458 let mut tree = ShadowTree::new();
459 let mut layout = LayoutEngine::new();
460
461 let root_id = NodeId(1);
463 let child_id = NodeId(2);
464
465 let mut root_props = HashMap::new();
466 root_props.insert("title".into(), PropValue::String("Hello".into()));
467
468 tree.create_node(root_id, ViewType::Container, root_props);
469 tree.create_node(child_id, ViewType::Text, HashMap::new());
470 tree.set_root(root_id);
471 tree.append_child(root_id, child_id);
472
473 layout.create_node(root_id, &Default::default()).unwrap();
474 layout.create_node(child_id, &Default::default()).unwrap();
475
476 let snap = snapshot_tree(&tree, &layout).unwrap();
477 assert_eq!(snap.id, 1);
478 assert_eq!(snap.children.len(), 1);
479 assert_eq!(snap.children[0].id, 2);
480 assert_eq!(snap.props.get("title").unwrap(), "Hello");
481 }
482
483 #[test]
484 fn test_layout_overlay_empty() {
485 let tree = ShadowTree::new();
486 let layout = LayoutEngine::new();
487 let overlay = gather_overlay(&tree, &layout, None);
488 assert!(overlay.rects.is_empty());
489 }
490
491 #[test]
492 fn test_frame_timer() {
493 let mut timer = FrameTimer::start();
494 timer.record_layout(Duration::from_millis(5));
495 timer.record_commit();
496 timer.record_commit();
497 thread::sleep(Duration::from_millis(1));
499 let record = timer.finish(1, 10);
500 assert_eq!(record.frame_number, 1);
501 assert_eq!(record.commit_count, 2);
502 assert_eq!(record.node_count, 10);
503 assert!(record.total_ms >= 1.0);
504 assert!(record.layout_ms >= 4.5); }
506
507 #[test]
508 fn test_profiler_summary() {
509 let mut profiler = Profiler::new(100);
510 profiler.push_frame(FrameRecord {
511 frame_number: 1, total_ms: 10.0, layout_ms: 4.0, commit_count: 2, node_count: 50,
512 });
513 profiler.push_frame(FrameRecord {
514 frame_number: 2, total_ms: 20.0, layout_ms: 8.0, commit_count: 3, node_count: 55,
515 });
516
517 let summary = profiler.summary();
518 assert_eq!(summary.frame_count, 2);
519 assert!((summary.avg_frame_ms - 15.0).abs() < 0.01);
520 assert!((summary.avg_layout_ms - 6.0).abs() < 0.01);
521 assert!((summary.max_frame_ms - 20.0).abs() < 0.01);
522 assert_eq!(summary.total_commits, 5);
523 }
524
525 #[test]
526 fn test_profiler_rolling_window() {
527 let mut profiler = Profiler::new(3);
528 for i in 1..=5 {
529 profiler.push_frame(FrameRecord {
530 frame_number: i, total_ms: i as f64, layout_ms: 0.0,
531 commit_count: 1, node_count: 10,
532 });
533 }
534 assert_eq!(profiler.frames().len(), 3);
535 assert_eq!(profiler.frames()[0].frame_number, 3);
536 assert_eq!(profiler.frames()[2].frame_number, 5);
537 assert_eq!(profiler.total_frames(), 5);
538 }
539
540 #[test]
541 fn test_ir_recorder() {
542 use crate::ir::IrCommand;
543
544 let mut recorder = IrRecorder::new();
545 assert!(!recorder.is_recording());
546
547 recorder.start_recording();
548 assert!(recorder.is_recording());
549
550 let batch = IrBatch {
551 commit_id: 1,
552 timestamp_ms: 0.0,
553 commands: vec![IrCommand::SetRootNode { id: NodeId(1) }],
554 };
555 recorder.record(&batch);
556 recorder.record(&batch);
557
558 recorder.stop_recording();
559 assert_eq!(recorder.len(), 2);
560
561 let json = recorder.export_json();
562 assert_eq!(json["batch_count"], 2);
563 assert_eq!(json["version"], 1);
564 }
565
566 #[test]
567 fn test_devtools_protocol_roundtrip() {
568 let request_json = r#"{"type":"getTree"}"#;
569 let request: DevToolsRequest = serde_json::from_str(request_json).unwrap();
570 assert!(matches!(request, DevToolsRequest::GetTree));
571
572 let response = DevToolsResponse::Tree { root: None };
573 let json = serde_json::to_string(&response).unwrap();
574 assert!(json.contains("\"type\":\"tree\""));
575 }
576
577 #[test]
578 fn test_handle_devtools_request_get_tree() {
579 let tree = ShadowTree::new();
580 let layout = LayoutEngine::new();
581 let profiler = Profiler::new(100);
582 let mut recorder = IrRecorder::new();
583
584 let resp = handle_devtools_request(
585 &DevToolsRequest::GetTree, &tree, &layout, &profiler, &mut recorder,
586 );
587 match resp {
588 DevToolsResponse::Tree { root } => assert!(root.is_none()),
589 _ => panic!("expected Tree response"),
590 }
591 }
592
593 #[test]
594 fn test_handle_devtools_set_recording() {
595 let tree = ShadowTree::new();
596 let layout = LayoutEngine::new();
597 let profiler = Profiler::new(100);
598 let mut recorder = IrRecorder::new();
599
600 handle_devtools_request(
601 &DevToolsRequest::SetRecording { enabled: true },
602 &tree, &layout, &profiler, &mut recorder,
603 );
604 assert!(recorder.is_recording());
605
606 handle_devtools_request(
607 &DevToolsRequest::SetRecording { enabled: false },
608 &tree, &layout, &profiler, &mut recorder,
609 );
610 assert!(!recorder.is_recording());
611 }
612}