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"));
}
}