1use super::trace::Trace;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ChromeTrace {
11 #[serde(rename = "traceEvents")]
13 pub trace_events: Vec<ChromeTraceEvent>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ChromeTraceEvent {
19 pub name: String,
21 pub cat: String,
23 pub ph: String,
25 pub ts: u64,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub dur: Option<u64>,
30 pub pid: u32,
32 pub tid: u32,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub args: Option<serde_json::Value>,
37}
38
39impl ChromeTrace {
40 #[must_use]
42 pub fn from_trace(trace: &Trace) -> Self {
43 let mut events = Vec::new();
44
45 for span in &trace.spans {
46 if let Some(dur_ns) = span.duration_ns() {
47 events.push(ChromeTraceEvent {
48 name: span.name.clone(),
49 cat: span
50 .category
51 .clone()
52 .unwrap_or_else(|| "default".to_string()),
53 ph: "X".to_string(), ts: span.start_ns / 1000, dur: Some(dur_ns / 1000),
56 pid: 1,
57 tid: 1,
58 args: if span.metadata.is_empty() {
59 None
60 } else {
61 Some(serde_json::json!(span.metadata))
62 },
63 });
64 }
65 }
66
67 Self {
68 trace_events: events,
69 }
70 }
71
72 #[must_use]
74 pub fn to_json(&self) -> String {
75 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
76 }
77
78 #[must_use]
80 pub fn to_json_compact(&self) -> String {
81 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct FlameGraph {
88 stacks: Vec<FlameStack>,
90}
91
92#[derive(Debug, Clone)]
94pub struct FlameStack {
95 pub frames: Vec<String>,
97 pub value: f64,
99}
100
101impl FlameGraph {
102 #[must_use]
104 pub fn from_trace(trace: &Trace) -> Self {
105 let mut stacks = Vec::new();
107
108 for span in &trace.spans {
110 if let Some(dur_ns) = span.duration_ns() {
111 let mut frames = Vec::new();
112
113 frames.push(span.name.clone());
115
116 if let Some(parent_id) = span.parent {
118 if let Some(parent) = trace.spans.iter().find(|s| s.id == parent_id) {
119 frames.insert(0, parent.name.clone());
120 }
121 }
122
123 stacks.push(FlameStack {
124 frames,
125 value: dur_ns as f64 / 1_000_000.0,
126 });
127 }
128 }
129
130 Self { stacks }
131 }
132
133 #[must_use]
135 pub fn to_collapsed(&self) -> String {
136 let mut output = String::new();
137
138 for stack in &self.stacks {
139 let stack_str = stack.frames.join(";");
140 output.push_str(&format!("{} {}\n", stack_str, stack.value as u64));
141 }
142
143 output
144 }
145
146 #[must_use]
148 pub fn to_svg(&self, width: u32, height: u32) -> String {
149 let mut svg = format!(
150 r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
151 width, height, width, height
152 );
153
154 svg.push_str(
155 r#"
156 <style>
157 .frame { stroke: #333; stroke-width: 0.5; }
158 .frame:hover { stroke: #000; stroke-width: 1; }
159 text { font-family: monospace; font-size: 10px; fill: #333; }
160 </style>
161"#,
162 );
163
164 let total_value: f64 = self.stacks.iter().map(|s| s.value).sum();
166 if total_value > 0.0 {
167 let mut y = 0.0;
168 let bar_height = 20.0;
169
170 for stack in &self.stacks {
171 let w = (stack.value / total_value) * width as f64;
172 if w > 1.0 {
173 let color = random_color(stack.frames.last().unwrap_or(&String::new()));
174 svg.push_str(&format!(
175 r#" <rect class="frame" x="0" y="{}" width="{}" height="{}" fill="{}"><title>{}: {:.2}ms</title></rect>"#,
176 y, w, bar_height, color,
177 stack.frames.join(" → "), stack.value
178 ));
179 svg.push('\n');
180 y += bar_height;
181 }
182 }
183 }
184
185 svg.push_str("</svg>");
186 svg
187 }
188}
189
190fn random_color(s: &str) -> String {
192 let hash: u32 = s
193 .bytes()
194 .fold(0, |acc, b| acc.wrapping_add(b as u32).wrapping_mul(31));
195 let hue = hash % 360;
196 format!("hsl({}, 70%, 60%)", hue)
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CiMetrics {
202 pub span_count: usize,
204 pub duration_ms: f64,
206 pub functions: Vec<FunctionMetric>,
208 pub passed: bool,
210 pub failures: Vec<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct FunctionMetric {
217 pub name: String,
219 pub count: usize,
221 pub mean_ms: f64,
223 pub p99_ms: f64,
225 pub total_ms: f64,
227}
228
229impl CiMetrics {
230 #[must_use]
232 pub fn from_trace(trace: &Trace) -> Self {
233 let perf = super::metrics::PerformanceMetrics::from_trace(trace);
234
235 let functions: Vec<FunctionMetric> = perf
236 .function_times
237 .iter()
238 .map(|(name, stats)| FunctionMetric {
239 name: name.clone(),
240 count: stats.count,
241 mean_ms: stats.mean,
242 p99_ms: stats.p99,
243 total_ms: stats.mean * stats.count as f64,
244 })
245 .collect();
246
247 Self {
248 span_count: trace.span_count(),
249 duration_ms: trace
250 .duration
251 .map(|d| d.as_secs_f64() * 1000.0)
252 .unwrap_or(0.0),
253 functions,
254 passed: true,
255 failures: Vec::new(),
256 }
257 }
258
259 #[must_use]
261 pub fn check_thresholds(&self, max_p99_ms: f64) -> Self {
262 let mut result = self.clone();
263 result.failures.clear();
264 result.passed = true;
265
266 for func in &self.functions {
267 if func.p99_ms > max_p99_ms {
268 result.failures.push(format!(
269 "{}: p99 {:.2}ms exceeds threshold {:.2}ms",
270 func.name, func.p99_ms, max_p99_ms
271 ));
272 result.passed = false;
273 }
274 }
275
276 result
277 }
278
279 #[must_use]
281 pub fn to_json(&self) -> String {
282 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
283 }
284}
285
286#[cfg(test)]
287#[allow(clippy::unwrap_used, clippy::expect_used)]
288mod tests {
289 use super::*;
290 use crate::perf::trace::Tracer;
291
292 fn create_test_trace() -> Trace {
293 let mut tracer = Tracer::new();
294 tracer.start();
295
296 {
297 let _outer = tracer.span("render");
298 std::thread::sleep(std::time::Duration::from_micros(100));
299 {
300 let _inner = tracer.span("draw");
301 std::thread::sleep(std::time::Duration::from_micros(50));
302 }
303 }
304
305 tracer.stop()
306 }
307
308 #[test]
309 fn test_chrome_trace_from_trace() {
310 let trace = create_test_trace();
311 let chrome = ChromeTrace::from_trace(&trace);
312
313 assert_eq!(chrome.trace_events.len(), 2);
314 }
315
316 #[test]
317 fn test_chrome_trace_to_json() {
318 let trace = create_test_trace();
319 let chrome = ChromeTrace::from_trace(&trace);
320 let json = chrome.to_json();
321
322 assert!(json.contains("traceEvents"));
323 assert!(json.contains("render"));
324 assert!(json.contains("draw"));
325 }
326
327 #[test]
328 fn test_chrome_trace_event_fields() {
329 let trace = create_test_trace();
330 let chrome = ChromeTrace::from_trace(&trace);
331
332 let event = &chrome.trace_events[0];
333 assert!(!event.name.is_empty());
334 assert_eq!(event.ph, "X");
335 assert!(event.dur.is_some());
336 }
337
338 #[test]
339 fn test_flame_graph_from_trace() {
340 let trace = create_test_trace();
341 let flame = FlameGraph::from_trace(&trace);
342
343 assert!(!flame.stacks.is_empty());
344 }
345
346 #[test]
347 fn test_flame_graph_to_collapsed() {
348 let trace = create_test_trace();
349 let flame = FlameGraph::from_trace(&trace);
350 let collapsed = flame.to_collapsed();
351
352 assert!(!collapsed.is_empty());
353 assert!(collapsed.contains("render") || collapsed.contains("draw"));
354 }
355
356 #[test]
357 fn test_flame_graph_to_svg() {
358 let trace = create_test_trace();
359 let flame = FlameGraph::from_trace(&trace);
360 let svg = flame.to_svg(800, 400);
361
362 assert!(svg.starts_with("<svg"));
363 assert!(svg.ends_with("</svg>"));
364 assert!(svg.contains("rect"));
365 }
366
367 #[test]
368 fn test_ci_metrics_from_trace() {
369 let trace = create_test_trace();
370 let metrics = CiMetrics::from_trace(&trace);
371
372 assert_eq!(metrics.span_count, 2);
373 assert!(metrics.passed);
374 }
375
376 #[test]
377 fn test_ci_metrics_to_json() {
378 let trace = create_test_trace();
379 let metrics = CiMetrics::from_trace(&trace);
380 let json = metrics.to_json();
381
382 assert!(json.contains("span_count"));
383 assert!(json.contains("functions"));
384 }
385
386 #[test]
387 fn test_ci_metrics_check_thresholds() {
388 let trace = create_test_trace();
389 let metrics = CiMetrics::from_trace(&trace);
390
391 let _checked = metrics.check_thresholds(0.001);
393
394 let checked = metrics.check_thresholds(10000.0);
396 assert!(checked.passed);
397 }
398
399 #[test]
400 fn test_random_color() {
401 let c1 = random_color("test");
402 let c2 = random_color("test");
403 let c3 = random_color("other");
404
405 assert_eq!(c1, c2);
407 assert_ne!(c1, c3);
409 assert!(c1.starts_with("hsl("));
411 }
412}