layout_audit/output/
table.rs

1use crate::types::StructLayout;
2use colored::Colorize;
3use comfy_table::{Cell, CellAlignment, Color, Table, presets::UTF8_FULL_CONDENSED};
4
5pub struct TableFormatter {
6    no_color: bool,
7    cache_line_size: u32,
8}
9
10impl TableFormatter {
11    pub fn new(no_color: bool, cache_line_size: u32) -> Self {
12        Self { no_color, cache_line_size }
13    }
14
15    pub fn format(&self, layouts: &[StructLayout]) -> String {
16        let mut output = String::new();
17
18        for (i, layout) in layouts.iter().enumerate() {
19            if i > 0 {
20                output.push_str("\n\n");
21            }
22            output.push_str(&self.format_struct(layout));
23        }
24
25        output
26    }
27
28    fn format_struct(&self, layout: &StructLayout) -> String {
29        let mut output = String::new();
30
31        let header = format!(
32            "struct {} ({} bytes, {:.1}% padding, {} cache line{})",
33            layout.name,
34            layout.size,
35            layout.metrics.padding_percentage,
36            layout.metrics.cache_lines_spanned,
37            if layout.metrics.cache_lines_spanned == 1 { "" } else { "s" }
38        );
39
40        if self.no_color {
41            output.push_str(&header);
42        } else {
43            output.push_str(&header.bold().to_string());
44        }
45        output.push('\n');
46
47        if let Some(ref loc) = layout.source_location {
48            output.push_str(&format!("  defined at {}:{}\n", loc.file, loc.line));
49        }
50        output.push('\n');
51
52        let mut table = Table::new();
53        table.load_preset(UTF8_FULL_CONDENSED);
54        table.set_header(vec!["Offset", "Size", "Type", "Field"]);
55
56        let mut entries: Vec<TableEntry> = Vec::new();
57
58        let mut padding_iter = layout.metrics.padding_holes.iter().peekable();
59
60        for member in &layout.members {
61            while let Some(hole) = padding_iter.peek() {
62                if member.offset.map(|o| hole.offset < o).unwrap_or(false) {
63                    let hole = padding_iter.next().unwrap();
64                    entries.push(TableEntry::Padding { offset: hole.offset, size: hole.size });
65                } else {
66                    break;
67                }
68            }
69
70            entries.push(TableEntry::Member {
71                offset: member.offset,
72                size: member.size,
73                type_name: &member.type_name,
74                name: &member.name,
75                bit_offset: member.bit_offset,
76                bit_size: member.bit_size,
77            });
78        }
79
80        for hole in padding_iter {
81            entries.push(TableEntry::Padding { offset: hole.offset, size: hole.size });
82        }
83
84        entries.sort_by_key(|e| match e {
85            TableEntry::Member { offset, .. } => offset.unwrap_or(u64::MAX),
86            TableEntry::Padding { offset, .. } => *offset,
87        });
88
89        let mut last_cache_line: Option<u64> = None;
90
91        for entry in &entries {
92            let offset = match entry {
93                TableEntry::Member { offset: Some(o), .. } => Some(*o),
94                TableEntry::Member { offset: None, .. } => None,
95                TableEntry::Padding { offset, .. } => Some(*offset),
96            };
97
98            if let Some(off) = offset {
99                let current_cache_line = off / self.cache_line_size as u64;
100                if last_cache_line.is_some_and(|l| l != current_cache_line) {
101                    let marker_offset = current_cache_line * self.cache_line_size as u64;
102                    table.add_row(vec![
103                        Cell::new(format!(
104                            "--- cache line {} ({}) ---",
105                            current_cache_line, marker_offset
106                        ))
107                        .set_alignment(CellAlignment::Center),
108                        Cell::new(""),
109                        Cell::new(""),
110                        Cell::new(""),
111                    ]);
112                }
113                last_cache_line = Some(current_cache_line);
114            }
115
116            match entry {
117                TableEntry::Member { offset, size, type_name, name, bit_offset, bit_size } => {
118                    let offset_str = match (offset, bit_offset) {
119                        (Some(o), Some(bo)) => format!("{}:{}", o, bo),
120                        (Some(o), None) => o.to_string(),
121                        (None, Some(bo)) => format!("?:{}", bo),
122                        (None, None) => "?".to_string(),
123                    };
124                    let size_str = match (size, bit_size) {
125                        (_, Some(bs)) => format!("{}b", bs),
126                        (Some(s), None) => s.to_string(),
127                        (None, None) => "?".to_string(),
128                    };
129                    table.add_row(vec![
130                        Cell::new(offset_str),
131                        Cell::new(size_str),
132                        Cell::new(type_name.to_string()),
133                        Cell::new(name.to_string()),
134                    ]);
135                }
136                TableEntry::Padding { offset, size } => {
137                    let row = if self.no_color {
138                        vec![
139                            Cell::new(offset.to_string()),
140                            Cell::new(format!("[{} bytes]", size)),
141                            Cell::new("---"),
142                            Cell::new("PAD"),
143                        ]
144                    } else {
145                        vec![
146                            Cell::new(offset.to_string()).fg(Color::Yellow),
147                            Cell::new(format!("[{} bytes]", size)).fg(Color::Yellow),
148                            Cell::new("---").fg(Color::Yellow),
149                            Cell::new("PAD").fg(Color::Yellow),
150                        ]
151                    };
152                    table.add_row(row);
153                }
154            }
155        }
156
157        output.push_str(&table.to_string());
158
159        output.push_str(&format!(
160            "\n\nSummary: {} useful bytes, {} padding bytes ({:.1}%), cache density: {:.1}%\n",
161            layout.metrics.useful_size,
162            layout.metrics.padding_bytes,
163            layout.metrics.padding_percentage,
164            layout.metrics.cache_line_density
165        ));
166
167        if let Some(ref fs) = layout.metrics.false_sharing {
168            if !fs.spanning_warnings.is_empty() {
169                let header = "\nCache Line Spanning (severe):";
170                if self.no_color {
171                    output.push_str(header);
172                } else {
173                    output.push_str(&header.red().bold().to_string());
174                }
175                output.push('\n');
176
177                for w in &fs.spanning_warnings {
178                    let msg = format!(
179                        "  - '{}' ({}) at offset {} spans {} cache lines ({}-{})",
180                        w.member,
181                        w.type_name,
182                        w.offset,
183                        w.lines_spanned,
184                        w.start_cache_line,
185                        w.end_cache_line
186                    );
187                    if self.no_color {
188                        output.push_str(&msg);
189                    } else {
190                        output.push_str(&msg.red().to_string());
191                    }
192                    output.push('\n');
193                }
194            }
195
196            if !fs.warnings.is_empty() {
197                let header = "\nPotential False Sharing:";
198                if self.no_color {
199                    output.push_str(header);
200                } else {
201                    output.push_str(&header.yellow().bold().to_string());
202                }
203                output.push('\n');
204
205                for w in &fs.warnings {
206                    let gap_desc = match w.gap_bytes.cmp(&0) {
207                        std::cmp::Ordering::Less => format!("{} bytes overlap", -w.gap_bytes),
208                        std::cmp::Ordering::Equal => "adjacent".to_string(),
209                        std::cmp::Ordering::Greater => format!("{} byte gap", w.gap_bytes),
210                    };
211                    let msg = format!(
212                        "  - '{}' and '{}' share cache line {} ({})",
213                        w.member_a, w.member_b, w.cache_line, gap_desc
214                    );
215                    if self.no_color {
216                        output.push_str(&msg);
217                    } else {
218                        output.push_str(&msg.yellow().to_string());
219                    }
220                    output.push('\n');
221                }
222            }
223
224            if !fs.atomic_members.is_empty() {
225                let names: Vec<&str> = fs.atomic_members.iter().map(|m| m.name.as_str()).collect();
226                output.push_str(&format!("\nAtomic members: {}\n", names.join(", ")));
227            }
228        }
229
230        output
231    }
232}
233
234enum TableEntry<'a> {
235    Member {
236        offset: Option<u64>,
237        size: Option<u64>,
238        type_name: &'a str,
239        name: &'a str,
240        bit_offset: Option<u64>,
241        bit_size: Option<u64>,
242    },
243    Padding {
244        offset: u64,
245        size: u64,
246    },
247}