use padlock_core::analysis::impact::estimate_impact;
use padlock_core::ir::{StructLayout, TypeInfo, find_padding};
pub fn render_explain(layout: &StructLayout) -> String {
use padlock_core::analysis::reorder;
let mut out = String::new();
let loc = match (&layout.source_file, layout.source_line) {
(Some(f), Some(l)) => format!(" ({}:{})", f, l),
(Some(f), None) => format!(" ({})", f),
_ => String::new(),
};
out.push_str(&format!("{}{}\n", layout.name, loc));
out.push_str(&format!(
"{} bytes align={} fields={}{}{}\n",
layout.total_size,
layout.align,
layout.fields.len(),
if layout.is_packed { " [packed]" } else { "" },
if layout.is_repr_rust {
" [repr(Rust) — compiler may reorder]"
} else {
""
},
));
let col_field = 36usize;
let divider = format!(
"├{:─<8}┼{:─<6}┼{:─<7}┼{:─<4}┼{:─<col_field$}┤",
"", "", "", "", ""
);
let top = format!(
"┌{:─<8}┬{:─<6}┬{:─<7}┬{:─<4}┬{:─<col_field$}┐",
"", "", "", "", ""
);
let bot = format!(
"└{:─<8}┴{:─<6}┴{:─<7}┴{:─<4}┴{:─<col_field$}┘",
"", "", "", "", ""
);
let header = format!(
"│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│",
"offset", "size", "align", "CL", "field"
);
out.push_str(&top);
out.push('\n');
out.push_str(&header);
out.push('\n');
out.push_str(÷r);
out.push('\n');
#[derive(Debug)]
enum Row {
Field {
offset: usize,
size: usize,
align: usize,
name: String,
ty: String,
},
Pad {
offset: usize,
size: usize,
trailing: bool,
},
CacheLine {
line_number: usize,
offset: usize,
},
}
let cache_line = layout.arch.cache_line_size;
let mut rows: Vec<Row> = Vec::new();
let gaps = find_padding(layout);
let last_field_name = layout.fields.last().map(|f| f.name.as_str()).unwrap_or("");
let mut last_cache_line: Option<usize> = None;
for field in &layout.fields {
let field_cache_line = field.offset / cache_line;
if last_cache_line.is_none_or(|prev| field_cache_line > prev) {
if last_cache_line.is_some() {
rows.push(Row::CacheLine {
line_number: field_cache_line,
offset: field_cache_line * cache_line,
});
}
last_cache_line = Some(field_cache_line);
}
let ty_name = type_name(&field.ty);
rows.push(Row::Field {
offset: field.offset,
size: field.size,
align: field.align,
name: field.name.clone(),
ty: ty_name,
});
if let Some(gap) = gaps.iter().find(|g| g.after_field == field.name) {
let pad_offset = field.offset + field.size;
let is_trailing = field.name == last_field_name;
rows.push(Row::Pad {
offset: pad_offset,
size: gap.bytes,
trailing: is_trailing,
});
}
}
let cache_sep_inner = 8 + 1 + 6 + 1 + 7 + 1 + 4 + 1 + col_field + 3; for row in &rows {
match row {
Row::Field {
offset,
size,
align,
name,
ty,
} => {
let cl = offset / cache_line;
let label = format!("{}: {}", name, ty);
let label = if label.len() > col_field {
format!("{}…", &label[..col_field - 1])
} else {
label
};
out.push_str(&format!(
"│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│\n",
offset, size, align, cl, label
));
}
Row::Pad {
offset,
size,
trailing,
} => {
let cl = offset / cache_line;
let label = if *trailing {
"<padding> (trailing)".to_string()
} else {
"<padding>".to_string()
};
out.push_str(&format!(
"│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│\n",
offset, size, "—", cl, label
));
}
Row::CacheLine {
line_number,
offset,
} => {
let label = format!("── cache line {line_number} (offset {offset}) ");
let used = label.len();
let pad = if cache_sep_inner > used + 4 {
"═".repeat(cache_sep_inner - used - 4)
} else {
String::new()
};
out.push_str(&format!("╞{label}{pad}╡\n"));
}
}
}
out.push_str(&bot);
out.push('\n');
let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
if wasted > 0 && !layout.is_packed && !layout.is_union {
let pct = wasted as f64 / layout.total_size as f64 * 100.0;
let (opt_size, savings) = reorder::reorder_savings(layout);
if savings > 0 {
let opt_order: Vec<String> = reorder::optimal_order(layout)
.iter()
.map(|f| f.name.clone())
.collect();
out.push_str(&format!(
"{} bytes wasted ({:.0}%) — reorder: {} → {} bytes\n",
wasted,
pct,
opt_order.join(", "),
opt_size
));
const CACHE_LINE: usize = 64;
let impact = estimate_impact(savings, layout.total_size, opt_size, CACHE_LINE);
out.push_str(&format!(
" ~{savings} KB extra per 1K instances · ~{savings} MB per 1M \
instances · ~{cl_1m} extra cache lines/1M (seq. scan)\n",
cl_1m = fmt_count(impact.extra_cache_lines_1m),
));
if impact.reduces_cache_line_crossings() {
out.push_str(&format!(
" Spans {} cache line(s); optimal spans {}\n",
impact.current_cache_lines, impact.optimal_cache_lines,
));
}
} else {
out.push_str(&format!(
"{} bytes wasted ({:.0}%) — already in optimal order\n",
wasted, pct
));
}
} else if layout.is_packed {
out.push_str("packed — no padding\n");
} else {
out.push_str("no layout issues — struct is already optimally laid out\n");
}
out
}
fn fmt_count(n: usize) -> String {
if n >= 1_000_000 {
format!("{}M", n / 1_000_000)
} else if n >= 1_000 {
format!("{}K", n / 1_000)
} else {
n.to_string()
}
}
fn type_name(ty: &TypeInfo) -> String {
match ty {
TypeInfo::Primitive { name, .. } => name.clone(),
TypeInfo::Pointer { .. } => "*ptr".to_string(),
TypeInfo::Array { element, count, .. } => format!("[{}; {}]", type_name(element), count),
TypeInfo::Struct(inner) => inner.name.clone(),
TypeInfo::Opaque { name, .. } => name.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use padlock_core::ir::test_fixtures::connection_layout;
#[test]
fn explain_contains_field_names() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("timeout"));
assert!(out.contains("port"));
assert!(out.contains("is_active"));
assert!(out.contains("is_tls"));
}
#[test]
fn explain_shows_padding_rows() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("<padding>"));
}
#[test]
fn explain_shows_struct_size() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("24 bytes"));
}
#[test]
fn explain_shows_reorder_suggestion() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("reorder"));
assert!(out.contains("→"));
}
#[test]
fn explain_shows_impact_scale_line() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("~8 KB extra per 1K instances"));
assert!(out.contains("~8 MB per 1M instances"));
assert!(out.contains("extra cache lines/1M"));
}
#[test]
fn explain_no_impact_line_when_no_savings() {
let layout = padlock_core::ir::test_fixtures::packed_layout();
let out = render_explain(&layout);
assert!(!out.contains("KB extra per 1K"));
assert!(!out.contains("MB per 1M"));
}
#[test]
fn explain_shows_cache_line_separator_when_struct_spans_multiple_lines() {
use padlock_core::arch::X86_64_SYSV;
use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
let big = StructLayout {
name: "Big".to_string(),
total_size: 128,
align: 8,
fields: vec![
Field {
name: "a".to_string(),
ty: TypeInfo::Primitive {
name: "u8[60]".to_string(),
size: 60,
align: 1,
},
offset: 0,
size: 60,
align: 1,
source_file: None,
source_line: None,
access: AccessPattern::Unknown,
},
Field {
name: "b".to_string(),
ty: TypeInfo::Primitive {
name: "u64".to_string(),
size: 8,
align: 8,
},
offset: 64,
size: 8,
align: 8,
source_file: None,
source_line: None,
access: AccessPattern::Unknown,
},
],
source_file: None,
source_line: None,
arch: &X86_64_SYSV,
is_packed: false,
is_union: false,
is_repr_rust: false,
suppressed_findings: Vec::new(),
uncertain_fields: Vec::new(),
};
let out = render_explain(&big);
assert!(
out.contains("cache line 1"),
"must show cache line 1 separator: {out}"
);
}
#[test]
fn explain_shows_cl_column_header() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("CL"), "CL column header must appear");
}
#[test]
fn explain_cl_column_shows_zero_for_small_struct() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(out.contains("│ 0 │"), "all fields must be on cache line 0");
assert!(
!out.contains("│ 1 │"),
"no field should be on cache line 1"
);
}
#[test]
fn explain_cl_column_shows_nonzero_for_large_struct() {
use padlock_core::arch::X86_64_SYSV;
use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
let big = StructLayout {
name: "Big".to_string(),
total_size: 128,
align: 8,
fields: vec![
Field {
name: "a".to_string(),
ty: TypeInfo::Primitive {
name: "u8[64]".to_string(),
size: 64,
align: 1,
},
offset: 0,
size: 64,
align: 1,
source_file: None,
source_line: None,
access: AccessPattern::Unknown,
},
Field {
name: "b".to_string(),
ty: TypeInfo::Primitive {
name: "u64".to_string(),
size: 8,
align: 8,
},
offset: 64,
size: 8,
align: 8,
source_file: None,
source_line: None,
access: AccessPattern::Unknown,
},
],
source_file: None,
source_line: None,
arch: &X86_64_SYSV,
is_packed: false,
is_union: false,
is_repr_rust: false,
suppressed_findings: Vec::new(),
uncertain_fields: Vec::new(),
};
let out = render_explain(&big);
assert!(out.contains("│ 1 │"), "field b must show CL 1");
}
#[test]
fn explain_no_cache_line_separator_for_small_struct() {
let layout = connection_layout();
let out = render_explain(&layout);
assert!(
!out.contains("cache line 1"),
"single-cache-line struct must not show separator"
);
}
#[test]
fn fmt_count_formats_correctly() {
assert_eq!(fmt_count(999), "999");
assert_eq!(fmt_count(1_000), "1K");
assert_eq!(fmt_count(125_000), "125K");
assert_eq!(fmt_count(1_000_000), "1M");
assert_eq!(fmt_count(2_500_000), "2M");
}
}