layout-audit 0.5.0

Analyze binary memory layouts to detect padding inefficiencies
Documentation
use crate::types::StructLayout;
use colored::Colorize;
use comfy_table::{Cell, CellAlignment, Color, Table, presets::UTF8_FULL_CONDENSED};

pub struct TableFormatter {
    no_color: bool,
    cache_line_size: u32,
}

impl TableFormatter {
    pub fn new(no_color: bool, cache_line_size: u32) -> Self {
        Self { no_color, cache_line_size }
    }

    pub fn format(&self, layouts: &[StructLayout]) -> String {
        let mut output = String::new();

        for (i, layout) in layouts.iter().enumerate() {
            if i > 0 {
                output.push_str("\n\n");
            }
            output.push_str(&self.format_struct(layout));
        }

        output
    }

    fn format_struct(&self, layout: &StructLayout) -> String {
        let mut output = String::new();

        let header = format!(
            "struct {} ({} bytes, {:.1}% padding, {} cache line{})",
            layout.name,
            layout.size,
            layout.metrics.padding_percentage,
            layout.metrics.cache_lines_spanned,
            if layout.metrics.cache_lines_spanned == 1 { "" } else { "s" }
        );

        if self.no_color {
            output.push_str(&header);
        } else {
            output.push_str(&header.bold().to_string());
        }
        output.push('\n');

        if let Some(ref loc) = layout.source_location {
            output.push_str(&format!("  defined at {}:{}\n", loc.file, loc.line));
        }
        output.push('\n');

        let mut table = Table::new();
        table.load_preset(UTF8_FULL_CONDENSED);
        table.set_header(vec!["Offset", "Size", "Type", "Field"]);

        let mut entries: Vec<TableEntry> = Vec::new();

        let mut padding_iter = layout.metrics.padding_holes.iter().peekable();

        for member in &layout.members {
            while let Some(hole) = padding_iter.peek() {
                if member.offset.map(|o| hole.offset < o).unwrap_or(false) {
                    let hole = padding_iter.next().unwrap();
                    entries.push(TableEntry::Padding { offset: hole.offset, size: hole.size });
                } else {
                    break;
                }
            }

            entries.push(TableEntry::Member {
                offset: member.offset,
                size: member.size,
                type_name: &member.type_name,
                name: &member.name,
                bit_offset: member.bit_offset,
                bit_size: member.bit_size,
            });
        }

        for hole in padding_iter {
            entries.push(TableEntry::Padding { offset: hole.offset, size: hole.size });
        }

        entries.sort_by_key(|e| match e {
            TableEntry::Member { offset, .. } => offset.unwrap_or(u64::MAX),
            TableEntry::Padding { offset, .. } => *offset,
        });

        let mut last_cache_line: Option<u64> = None;

        for entry in &entries {
            let offset = match entry {
                TableEntry::Member { offset: Some(o), .. } => Some(*o),
                TableEntry::Member { offset: None, .. } => None,
                TableEntry::Padding { offset, .. } => Some(*offset),
            };

            if let Some(off) = offset {
                let current_cache_line = off / self.cache_line_size as u64;
                if last_cache_line.is_some_and(|l| l != current_cache_line) {
                    let marker_offset = current_cache_line * self.cache_line_size as u64;
                    table.add_row(vec![
                        Cell::new(format!(
                            "--- cache line {} ({}) ---",
                            current_cache_line, marker_offset
                        ))
                        .set_alignment(CellAlignment::Center),
                        Cell::new(""),
                        Cell::new(""),
                        Cell::new(""),
                    ]);
                }
                last_cache_line = Some(current_cache_line);
            }

            match entry {
                TableEntry::Member { offset, size, type_name, name, bit_offset, bit_size } => {
                    let offset_str = match (offset, bit_offset) {
                        (Some(o), Some(bo)) => format!("{}:{}", o, bo),
                        (Some(o), None) => o.to_string(),
                        (None, Some(bo)) => format!("?:{}", bo),
                        (None, None) => "?".to_string(),
                    };
                    let size_str = match (size, bit_size) {
                        (_, Some(bs)) => format!("{}b", bs),
                        (Some(s), None) => s.to_string(),
                        (None, None) => "?".to_string(),
                    };
                    table.add_row(vec![
                        Cell::new(offset_str),
                        Cell::new(size_str),
                        Cell::new(type_name.to_string()),
                        Cell::new(name.to_string()),
                    ]);
                }
                TableEntry::Padding { offset, size } => {
                    let row = if self.no_color {
                        vec![
                            Cell::new(offset.to_string()),
                            Cell::new(format!("[{} bytes]", size)),
                            Cell::new("---"),
                            Cell::new("PAD"),
                        ]
                    } else {
                        vec![
                            Cell::new(offset.to_string()).fg(Color::Yellow),
                            Cell::new(format!("[{} bytes]", size)).fg(Color::Yellow),
                            Cell::new("---").fg(Color::Yellow),
                            Cell::new("PAD").fg(Color::Yellow),
                        ]
                    };
                    table.add_row(row);
                }
            }
        }

        output.push_str(&table.to_string());

        output.push_str(&format!(
            "\n\nSummary: {} useful bytes, {} padding bytes ({:.1}%), cache density: {:.1}%\n",
            layout.metrics.useful_size,
            layout.metrics.padding_bytes,
            layout.metrics.padding_percentage,
            layout.metrics.cache_line_density
        ));

        if let Some(ref fs) = layout.metrics.false_sharing {
            if !fs.spanning_warnings.is_empty() {
                let header = "\nCache Line Spanning (severe):";
                if self.no_color {
                    output.push_str(header);
                } else {
                    output.push_str(&header.red().bold().to_string());
                }
                output.push('\n');

                for w in &fs.spanning_warnings {
                    let msg = format!(
                        "  - '{}' ({}) at offset {} spans {} cache lines ({}-{})",
                        w.member,
                        w.type_name,
                        w.offset,
                        w.lines_spanned,
                        w.start_cache_line,
                        w.end_cache_line
                    );
                    if self.no_color {
                        output.push_str(&msg);
                    } else {
                        output.push_str(&msg.red().to_string());
                    }
                    output.push('\n');
                }
            }

            if !fs.warnings.is_empty() {
                let header = "\nPotential False Sharing:";
                if self.no_color {
                    output.push_str(header);
                } else {
                    output.push_str(&header.yellow().bold().to_string());
                }
                output.push('\n');

                for w in &fs.warnings {
                    let gap_desc = match w.gap_bytes.cmp(&0) {
                        std::cmp::Ordering::Less => format!("{} bytes overlap", -w.gap_bytes),
                        std::cmp::Ordering::Equal => "adjacent".to_string(),
                        std::cmp::Ordering::Greater => format!("{} byte gap", w.gap_bytes),
                    };
                    let msg = format!(
                        "  - '{}' and '{}' share cache line {} ({})",
                        w.member_a, w.member_b, w.cache_line, gap_desc
                    );
                    if self.no_color {
                        output.push_str(&msg);
                    } else {
                        output.push_str(&msg.yellow().to_string());
                    }
                    output.push('\n');
                }
            }

            if !fs.atomic_members.is_empty() {
                let names: Vec<&str> = fs.atomic_members.iter().map(|m| m.name.as_str()).collect();
                output.push_str(&format!("\nAtomic members: {}\n", names.join(", ")));
            }
        }

        output
    }
}

enum TableEntry<'a> {
    Member {
        offset: Option<u64>,
        size: Option<u64>,
        type_name: &'a str,
        name: &'a str,
        bit_offset: Option<u64>,
        bit_size: Option<u64>,
    },
    Padding {
        offset: u64,
        size: u64,
    },
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{
        CacheLineSpanningWarning, FalseSharingAnalysis, FalseSharingWarning, LayoutMetrics,
        MemberLayout, PaddingHole, StructLayout,
    };

    fn sample_layout() -> StructLayout {
        let mut layout = StructLayout::new("Foo".to_string(), 16, Some(8));
        layout.members = vec![
            MemberLayout::new("a".to_string(), "u8".to_string(), Some(0), Some(1)),
            MemberLayout::new("b".to_string(), "u32".to_string(), Some(4), Some(4)),
        ];
        layout.metrics = LayoutMetrics {
            total_size: 16,
            useful_size: 5,
            padding_bytes: 11,
            padding_percentage: 68.75,
            cache_lines_spanned: 1,
            cache_line_density: 31.25,
            padding_holes: vec![PaddingHole {
                offset: 1,
                size: 3,
                after_member: Some("a".to_string()),
            }],
            false_sharing: Some(FalseSharingAnalysis {
                warnings: vec![FalseSharingWarning {
                    member_a: "a".to_string(),
                    member_b: "b".to_string(),
                    cache_line: 0,
                    gap_bytes: 0,
                }],
                spanning_warnings: vec![CacheLineSpanningWarning {
                    member: "b".to_string(),
                    type_name: "u32".to_string(),
                    offset: 4,
                    size: 4,
                    start_cache_line: 0,
                    end_cache_line: 1,
                    lines_spanned: 2,
                }],
                atomic_members: vec![crate::types::AtomicMember {
                    name: "a".to_string(),
                    type_name: "u8".to_string(),
                    offset: 0,
                    size: 1,
                    cache_line: 0,
                    end_cache_line: 0,
                    spans_cache_lines: false,
                }],
            }),
            partial: false,
        };
        layout
    }

    #[test]
    fn table_formatter_no_color_includes_padding_and_warnings() {
        let formatter = TableFormatter::new(true, 64);
        let out = formatter.format(&[sample_layout()]);
        assert!(out.contains("struct Foo"));
        assert!(out.contains("PAD"));
        assert!(out.contains("Potential False Sharing"));
        assert!(out.contains("Cache Line Spanning"));
        assert!(out.contains("Atomic members"));
    }

    #[test]
    fn table_formatter_color_path_runs() {
        let formatter = TableFormatter::new(false, 64);
        let out = formatter.format(&[sample_layout()]);
        assert!(out.contains("struct Foo"));
    }
}