Skip to main content

ghostscope_ui/events/
trace_display.rs

1use ghostscope_protocol::{
2    trace_event::{backtrace_error_label, BacktraceStatus},
3    ParsedInstruction, ParsedTraceEvent,
4};
5
6/// Runtime trace event after conversion into display items.
7///
8/// This keeps the UI transport structured without changing the eBPF/protocol
9/// wire format. Backtraces, variables, and runtime expression errors keep their
10/// fields for dedicated CLI/TUI rendering.
11#[derive(Debug, Clone)]
12pub struct UiTraceEvent {
13    pub trace_id: u64,
14    pub timestamp: u64,
15    pub pid: u32,
16    pub tid: u32,
17    pub items: Vec<TraceDisplayItem>,
18    pub execution_status: Option<u8>,
19}
20
21impl UiTraceEvent {
22    pub fn from_protocol_event(event: &ParsedTraceEvent) -> Self {
23        let execution_status = event.instructions.iter().rev().find_map(|instruction| {
24            if let ghostscope_protocol::ParsedInstruction::EndInstruction {
25                execution_status, ..
26            } = instruction
27            {
28                Some(*execution_status)
29            } else {
30                None
31            }
32        });
33
34        Self {
35            trace_id: event.trace_id,
36            timestamp: event.timestamp,
37            pid: event.pid,
38            tid: event.tid,
39            items: protocol_instructions_to_display_items(&event.instructions),
40            execution_status,
41        }
42    }
43
44    pub fn text_event(
45        trace_id: u64,
46        timestamp: u64,
47        pid: u32,
48        tid: u32,
49        content: String,
50        execution_status: Option<u8>,
51    ) -> Self {
52        Self {
53            trace_id,
54            timestamp,
55            pid,
56            tid,
57            items: vec![TraceDisplayItem::Text { content }],
58            execution_status,
59        }
60    }
61
62    pub fn to_formatted_output(&self) -> Vec<String> {
63        self.items
64            .iter()
65            .flat_map(TraceDisplayItem::to_formatted_output)
66            .collect()
67    }
68
69    pub fn is_error(&self) -> bool {
70        self.execution_status
71            .is_some_and(|status| status == 1 || status == 2)
72            || self.items.iter().any(|item| match item {
73                TraceDisplayItem::ExprError(_) => true,
74                TraceDisplayItem::Backtrace(backtrace) => {
75                    backtrace.status != BacktraceStatus::Complete
76                        && backtrace.status != BacktraceStatus::Truncated
77                }
78                _ => false,
79            })
80    }
81}
82
83#[derive(Debug, Clone)]
84pub enum TraceDisplayItem {
85    Text { content: String },
86    FormattedText { content: String },
87    Variable(VariableDisplay),
88    ComplexVariable(ComplexVariableDisplay),
89    ExprError(ExprErrorDisplay),
90    Backtrace(BacktraceDisplay),
91}
92
93impl TraceDisplayItem {
94    pub fn to_formatted_output(&self) -> Vec<String> {
95        match self {
96            Self::Text { content } => vec![content.clone()],
97            Self::FormattedText { content } => vec![content.clone()],
98            Self::Variable(variable) => vec![variable.to_formatted_output()],
99            Self::ComplexVariable(variable) => vec![variable.to_formatted_output()],
100            Self::ExprError(error) => vec![error.to_formatted_output()],
101            Self::Backtrace(backtrace) => backtrace.to_formatted_output(),
102        }
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct VariableDisplay {
108    pub name: String,
109    pub type_name: String,
110    pub formatted_value: String,
111}
112
113impl VariableDisplay {
114    pub fn to_formatted_output(&self) -> String {
115        format!(
116            "{} ({}): {}",
117            self.name, self.type_name, self.formatted_value
118        )
119    }
120}
121
122#[derive(Debug, Clone)]
123pub struct ComplexVariableDisplay {
124    pub name: String,
125    pub access_path: String,
126    pub type_index: u16,
127    pub formatted_value: String,
128}
129
130impl ComplexVariableDisplay {
131    pub fn display_name(&self) -> &str {
132        if self.access_path.is_empty() {
133            &self.name
134        } else {
135            &self.access_path
136        }
137    }
138
139    pub fn to_formatted_output(&self) -> String {
140        self.formatted_value.clone()
141    }
142}
143
144#[derive(Debug, Clone)]
145pub struct ExprErrorDisplay {
146    pub expr: String,
147    pub error_code: u8,
148    pub flags: u8,
149    pub failing_addr: u64,
150}
151
152impl ExprErrorDisplay {
153    pub fn reason(&self) -> &'static str {
154        match self.error_code {
155            1 => "null deref",
156            2 => "read error",
157            3 => "access error",
158            4 => "truncated",
159            5 => "offsets unavailable",
160            6 => "zero length",
161            _ => "error",
162        }
163    }
164
165    pub fn readable_flags(&self) -> Option<String> {
166        if self.flags == 0 {
167            return None;
168        }
169        let mut tags: Vec<&'static str> = Vec::new();
170        let is_memcmp = self.expr.contains("memcmp(");
171        let is_strncmp = self.expr.contains("strncmp(") || self.expr.contains("starts_with(");
172        if is_memcmp {
173            if (self.flags & 0x01) != 0 {
174                tags.push("first-arg read-fail");
175            }
176            if (self.flags & 0x02) != 0 {
177                tags.push("second-arg read-fail");
178            }
179            if (self.flags & 0x04) != 0 {
180                tags.push("len-clamped");
181            }
182            if (self.flags & 0x08) != 0 {
183                tags.push("len=0");
184            }
185        } else if is_strncmp {
186            if (self.flags & 0x01) != 0 {
187                tags.push("read-fail");
188            }
189            if (self.flags & 0x04) != 0 {
190                tags.push("len-clamped");
191            }
192            if (self.flags & 0x08) != 0 {
193                tags.push("len=0");
194            }
195        } else {
196            return Some(format!("0x{:02x}", self.flags));
197        }
198
199        (!tags.is_empty()).then(|| tags.join(","))
200    }
201
202    pub fn addr_text(&self) -> String {
203        if self.failing_addr != 0 {
204            format!("at 0x{:016x}", self.failing_addr)
205        } else {
206            "at NULL".to_string()
207        }
208    }
209
210    pub fn to_formatted_output(&self) -> String {
211        let base = format!(
212            "ExprError: {} ({} {}",
213            self.expr,
214            self.reason(),
215            self.addr_text()
216        );
217        match self.readable_flags() {
218            Some(flags) => format!("{base}, flags: {flags})"),
219            None => format!("{base})"),
220        }
221    }
222}
223
224fn protocol_instructions_to_display_items(
225    instructions: &[ParsedInstruction],
226) -> Vec<TraceDisplayItem> {
227    let mut items = Vec::new();
228    let mut index = 0usize;
229
230    while index < instructions.len() {
231        match &instructions[index] {
232            ParsedInstruction::PrintString { content } => {
233                if content.contains("{}") {
234                    let (formatted, consumed) =
235                        format_string_with_variable_items(content, instructions, index + 1);
236                    items.push(TraceDisplayItem::FormattedText { content: formatted });
237                    index += consumed;
238                } else {
239                    items.push(TraceDisplayItem::Text {
240                        content: content.clone(),
241                    });
242                    index += 1;
243                }
244            }
245            ParsedInstruction::PrintVariable {
246                name,
247                type_encoding,
248                formatted_value,
249                ..
250            } => {
251                items.push(TraceDisplayItem::Variable(VariableDisplay {
252                    name: name.clone(),
253                    type_name: format!("{type_encoding:?}"),
254                    formatted_value: formatted_value.clone(),
255                }));
256                index += 1;
257            }
258            ParsedInstruction::ExprError {
259                expr,
260                error_code,
261                flags,
262                failing_addr,
263            } => {
264                items.push(TraceDisplayItem::ExprError(ExprErrorDisplay {
265                    expr: expr.clone(),
266                    error_code: *error_code,
267                    flags: *flags,
268                    failing_addr: *failing_addr,
269                }));
270                index += 1;
271            }
272            ParsedInstruction::PrintComplexFormat { formatted_output } => {
273                items.push(TraceDisplayItem::FormattedText {
274                    content: formatted_output.clone(),
275                });
276                index += 1;
277            }
278            ParsedInstruction::PrintComplexVariable {
279                name,
280                access_path,
281                type_index,
282                formatted_value,
283                ..
284            } => {
285                items.push(TraceDisplayItem::ComplexVariable(ComplexVariableDisplay {
286                    name: name.clone(),
287                    access_path: access_path.clone(),
288                    type_index: *type_index,
289                    formatted_value: formatted_value.clone(),
290                }));
291                index += 1;
292            }
293            ParsedInstruction::Backtrace { .. } => {
294                index += 1;
295            }
296            ParsedInstruction::EndInstruction { .. } => {
297                index += 1;
298            }
299        }
300    }
301
302    items
303}
304
305fn format_string_with_variable_items(
306    format_string: &str,
307    instructions: &[ParsedInstruction],
308    start_index: usize,
309) -> (String, usize) {
310    let placeholder_count = format_string.matches("{}").count();
311    let mut consumed = 1;
312    let mut result = String::with_capacity(format_string.len());
313    let mut remaining = format_string;
314
315    for instruction_index in start_index..(start_index + placeholder_count).min(instructions.len())
316    {
317        let Some(pos) = remaining.find("{}") else {
318            break;
319        };
320
321        if let Some(ParsedInstruction::PrintVariable {
322            formatted_value, ..
323        }) = instructions.get(instruction_index)
324        {
325            result.push_str(&remaining[..pos]);
326            result.push_str(formatted_value);
327            consumed += 1;
328            remaining = &remaining[pos + 2..];
329        } else {
330            break;
331        }
332    }
333    result.push_str(remaining);
334
335    (result, consumed)
336}
337
338#[derive(Debug, Clone)]
339pub struct BacktraceDisplay {
340    pub requested_depth: u8,
341    pub physical_frame_count: usize,
342    pub status: BacktraceStatus,
343    pub error_code: u16,
344    pub raw: bool,
345    pub frames: Vec<BacktraceDisplayFrame>,
346}
347
348impl BacktraceDisplay {
349    pub fn header_text(&self) -> String {
350        let frame_word = if self.physical_frame_count == 1 {
351            "frame"
352        } else {
353            "frames"
354        };
355        format!(
356            "backtrace: {}, {} {} (max {})",
357            self.status.label(),
358            self.physical_frame_count,
359            frame_word,
360            self.requested_depth
361        )
362    }
363
364    pub fn stopped_text(&self) -> Option<String> {
365        if self.status == BacktraceStatus::Complete {
366            return None;
367        }
368
369        let suffix = match backtrace_error_label(self.error_code) {
370            Some("unknown") => format!(" (code={})", self.error_code),
371            Some(label) => format!(" ({label}, code={})", self.error_code),
372            None => String::new(),
373        };
374        Some(format!("stopped: {}{}", self.status.label(), suffix))
375    }
376
377    pub fn to_formatted_output(&self) -> Vec<String> {
378        let mut output = Vec::with_capacity(self.frames.len() + 2);
379        output.push(self.header_text());
380        output.extend(
381            self.frames
382                .iter()
383                .map(BacktraceDisplayFrame::to_formatted_output),
384        );
385        if let Some(stopped) = self.stopped_text() {
386            output.push(stopped);
387        }
388        output
389    }
390}
391
392#[derive(Debug, Clone)]
393pub struct BacktraceDisplayFrame {
394    pub index: usize,
395    pub inline: bool,
396    pub function: Option<String>,
397    pub parameters: Vec<String>,
398    pub address: Option<String>,
399    pub location: Option<String>,
400    pub module: String,
401    pub raw_ip: Option<u64>,
402    pub cookie: Option<u64>,
403    pub flags: Option<u16>,
404}
405
406impl BacktraceDisplayFrame {
407    pub fn to_formatted_output(&self) -> String {
408        let mut line = String::from("  #");
409        line.push_str(&self.index.to_string());
410        if self.inline {
411            line.push_str(".inline");
412        }
413        line.push(' ');
414
415        if let Some(function) = &self.function {
416            line.push_str(function);
417            if !self.parameters.is_empty() {
418                line.push('(');
419                line.push_str(&self.parameters.join(", "));
420                line.push(')');
421            }
422        } else if let Some(address) = &self.address {
423            line.push_str(address);
424        } else {
425            line.push_str("<unknown function>");
426        }
427
428        if let Some(location) = &self.location {
429            line.push_str(" at ");
430            line.push_str(location);
431        } else if self.function.is_some() {
432            line.push_str(" at ??");
433        }
434        line.push_str(" [");
435        line.push_str(&self.module);
436        line.push(']');
437
438        if let Some(raw_ip) = self.raw_ip {
439            line.push_str(&format!(" raw=0x{raw_ip:x}"));
440        }
441        if let Some(cookie) = self.cookie {
442            line.push_str(&format!(" cookie=0x{cookie:016x}"));
443        }
444        if let Some(flags) = self.flags {
445            line.push_str(&format!(" flags=0x{flags:x}"));
446        }
447
448        line
449    }
450}