1use padlock_core::analysis::impact::estimate_impact;
7use padlock_core::ir::{StructLayout, TypeInfo, find_padding};
8
9pub fn render_explain(layout: &StructLayout) -> String {
31 use padlock_core::analysis::reorder;
32
33 let mut out = String::new();
34
35 let loc = match (&layout.source_file, layout.source_line) {
37 (Some(f), Some(l)) => format!(" ({}:{})", f, l),
38 (Some(f), None) => format!(" ({})", f),
39 _ => String::new(),
40 };
41 out.push_str(&format!("{}{}\n", layout.name, loc));
42 out.push_str(&format!(
43 "{} bytes align={} fields={}{}{}\n",
44 layout.total_size,
45 layout.align,
46 layout.fields.len(),
47 if layout.is_packed { " [packed]" } else { "" },
48 if layout.is_repr_rust {
49 " [repr(Rust) — compiler may reorder]"
50 } else {
51 ""
52 },
53 ));
54
55 let col_field = 36usize;
57 let divider = format!(
58 "├{:─<8}┼{:─<6}┼{:─<7}┼{:─<4}┼{:─<col_field$}┤",
59 "", "", "", "", ""
60 );
61 let top = format!(
62 "┌{:─<8}┬{:─<6}┬{:─<7}┬{:─<4}┬{:─<col_field$}┐",
63 "", "", "", "", ""
64 );
65 let bot = format!(
66 "└{:─<8}┴{:─<6}┴{:─<7}┴{:─<4}┴{:─<col_field$}┘",
67 "", "", "", "", ""
68 );
69 let header = format!(
70 "│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│",
71 "offset", "size", "align", "CL", "field"
72 );
73
74 out.push_str(&top);
75 out.push('\n');
76 out.push_str(&header);
77 out.push('\n');
78 out.push_str(÷r);
79 out.push('\n');
80
81 #[derive(Debug)]
83 enum Row {
84 Field {
85 offset: usize,
86 size: usize,
87 align: usize,
88 name: String,
89 ty: String,
90 },
91 Pad {
92 offset: usize,
93 size: usize,
94 trailing: bool,
95 },
96 CacheLine {
97 line_number: usize,
98 offset: usize,
99 },
100 }
101
102 let cache_line = layout.arch.cache_line_size;
103 let mut rows: Vec<Row> = Vec::new();
104 let gaps = find_padding(layout);
105
106 let last_field_name = layout.fields.last().map(|f| f.name.as_str()).unwrap_or("");
107
108 let mut last_cache_line: Option<usize> = None;
110
111 for field in &layout.fields {
112 let field_cache_line = field.offset / cache_line;
113
114 if last_cache_line.is_none_or(|prev| field_cache_line > prev) {
116 if last_cache_line.is_some() {
117 rows.push(Row::CacheLine {
119 line_number: field_cache_line,
120 offset: field_cache_line * cache_line,
121 });
122 }
123 last_cache_line = Some(field_cache_line);
124 }
125
126 let ty_name = type_name(&field.ty);
127 rows.push(Row::Field {
128 offset: field.offset,
129 size: field.size,
130 align: field.align,
131 name: field.name.clone(),
132 ty: ty_name,
133 });
134 if let Some(gap) = gaps.iter().find(|g| g.after_field == field.name) {
135 let pad_offset = field.offset + field.size;
136 let is_trailing = field.name == last_field_name;
137 rows.push(Row::Pad {
138 offset: pad_offset,
139 size: gap.bytes,
140 trailing: is_trailing,
141 });
142 }
143 }
144
145 let cache_sep_inner = 8 + 1 + 6 + 1 + 7 + 1 + 4 + 1 + col_field + 3; for row in &rows {
149 match row {
150 Row::Field {
151 offset,
152 size,
153 align,
154 name,
155 ty,
156 } => {
157 let cl = offset / cache_line;
158 let label = format!("{}: {}", name, ty);
159 let label = if label.len() > col_field {
160 format!("{}…", &label[..col_field - 1])
161 } else {
162 label
163 };
164 out.push_str(&format!(
165 "│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│\n",
166 offset, size, align, cl, label
167 ));
168 }
169 Row::Pad {
170 offset,
171 size,
172 trailing,
173 } => {
174 let cl = offset / cache_line;
175 let label = if *trailing {
176 "<padding> (trailing)".to_string()
177 } else {
178 "<padding>".to_string()
179 };
180 out.push_str(&format!(
181 "│ {:>6} │ {:>4} │ {:>5} │ {:>2} │ {:<col_field$}│\n",
182 offset, size, "—", cl, label
183 ));
184 }
185 Row::CacheLine {
186 line_number,
187 offset,
188 } => {
189 let label = format!("── cache line {line_number} (offset {offset}) ");
190 let used = label.len();
192 let pad = if cache_sep_inner > used + 4 {
193 "═".repeat(cache_sep_inner - used - 4)
194 } else {
195 String::new()
196 };
197 out.push_str(&format!("╞{label}{pad}╡\n"));
198 }
199 }
200 }
201
202 out.push_str(&bot);
203 out.push('\n');
204
205 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
207
208 if wasted > 0 && !layout.is_packed && !layout.is_union {
209 let pct = wasted as f64 / layout.total_size as f64 * 100.0;
210 let (opt_size, savings) = reorder::reorder_savings(layout);
211 if savings > 0 {
212 let opt_order: Vec<String> = reorder::optimal_order(layout)
213 .iter()
214 .map(|f| f.name.clone())
215 .collect();
216 out.push_str(&format!(
217 "{} bytes wasted ({:.0}%) — reorder: {} → {} bytes\n",
218 wasted,
219 pct,
220 opt_order.join(", "),
221 opt_size
222 ));
223
224 const CACHE_LINE: usize = 64;
227 let impact = estimate_impact(savings, layout.total_size, opt_size, CACHE_LINE);
228 out.push_str(&format!(
229 " ~{savings} KB extra per 1K instances · ~{savings} MB per 1M \
230 instances · ~{cl_1m} extra cache lines/1M (seq. scan)\n",
231 cl_1m = fmt_count(impact.extra_cache_lines_1m),
232 ));
233 if impact.reduces_cache_line_crossings() {
234 out.push_str(&format!(
235 " Spans {} cache line(s); optimal spans {}\n",
236 impact.current_cache_lines, impact.optimal_cache_lines,
237 ));
238 }
239 } else {
240 out.push_str(&format!(
241 "{} bytes wasted ({:.0}%) — already in optimal order\n",
242 wasted, pct
243 ));
244 }
245 } else if layout.is_packed {
246 out.push_str("packed — no padding\n");
247 } else {
248 out.push_str("no layout issues — struct is already optimally laid out\n");
249 }
250
251 out
252}
253
254fn fmt_count(n: usize) -> String {
256 if n >= 1_000_000 {
257 format!("{}M", n / 1_000_000)
258 } else if n >= 1_000 {
259 format!("{}K", n / 1_000)
260 } else {
261 n.to_string()
262 }
263}
264
265fn type_name(ty: &TypeInfo) -> String {
266 match ty {
267 TypeInfo::Primitive { name, .. } => name.clone(),
268 TypeInfo::Pointer { .. } => "*ptr".to_string(),
269 TypeInfo::Array { element, count, .. } => format!("[{}; {}]", type_name(element), count),
270 TypeInfo::Struct(inner) => inner.name.clone(),
271 TypeInfo::Opaque { name, .. } => name.clone(),
272 }
273}
274
275#[cfg(test)]
278mod tests {
279 use super::*;
280 use padlock_core::ir::test_fixtures::connection_layout;
281
282 #[test]
283 fn explain_contains_field_names() {
284 let layout = connection_layout();
285 let out = render_explain(&layout);
286 assert!(out.contains("timeout"));
287 assert!(out.contains("port"));
288 assert!(out.contains("is_active"));
289 assert!(out.contains("is_tls"));
290 }
291
292 #[test]
293 fn explain_shows_padding_rows() {
294 let layout = connection_layout();
295 let out = render_explain(&layout);
296 assert!(out.contains("<padding>"));
297 }
298
299 #[test]
300 fn explain_shows_struct_size() {
301 let layout = connection_layout();
302 let out = render_explain(&layout);
303 assert!(out.contains("24 bytes"));
304 }
305
306 #[test]
307 fn explain_shows_reorder_suggestion() {
308 let layout = connection_layout();
309 let out = render_explain(&layout);
310 assert!(out.contains("reorder"));
311 assert!(out.contains("→"));
312 }
313
314 #[test]
315 fn explain_shows_impact_scale_line() {
316 let layout = connection_layout();
317 let out = render_explain(&layout);
318 assert!(out.contains("~8 KB extra per 1K instances"));
320 assert!(out.contains("~8 MB per 1M instances"));
321 assert!(out.contains("extra cache lines/1M"));
322 }
323
324 #[test]
325 fn explain_no_impact_line_when_no_savings() {
326 let layout = padlock_core::ir::test_fixtures::packed_layout();
327 let out = render_explain(&layout);
328 assert!(!out.contains("KB extra per 1K"));
329 assert!(!out.contains("MB per 1M"));
330 }
331
332 #[test]
333 fn explain_shows_cache_line_separator_when_struct_spans_multiple_lines() {
334 use padlock_core::arch::X86_64_SYSV;
335 use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
336 let big = StructLayout {
338 name: "Big".to_string(),
339 total_size: 128,
340 align: 8,
341 fields: vec![
342 Field {
343 name: "a".to_string(),
344 ty: TypeInfo::Primitive {
345 name: "u8[60]".to_string(),
346 size: 60,
347 align: 1,
348 },
349 offset: 0,
350 size: 60,
351 align: 1,
352 source_file: None,
353 source_line: None,
354 access: AccessPattern::Unknown,
355 },
356 Field {
357 name: "b".to_string(),
358 ty: TypeInfo::Primitive {
359 name: "u64".to_string(),
360 size: 8,
361 align: 8,
362 },
363 offset: 64,
364 size: 8,
365 align: 8,
366 source_file: None,
367 source_line: None,
368 access: AccessPattern::Unknown,
369 },
370 ],
371 source_file: None,
372 source_line: None,
373 arch: &X86_64_SYSV,
374 is_packed: false,
375 is_union: false,
376 is_repr_rust: false,
377 suppressed_findings: Vec::new(),
378 uncertain_fields: Vec::new(),
379 };
380 let out = render_explain(&big);
381 assert!(
382 out.contains("cache line 1"),
383 "must show cache line 1 separator: {out}"
384 );
385 }
386
387 #[test]
388 fn explain_shows_cl_column_header() {
389 let layout = connection_layout();
390 let out = render_explain(&layout);
391 assert!(out.contains("CL"), "CL column header must appear");
392 }
393
394 #[test]
395 fn explain_cl_column_shows_zero_for_small_struct() {
396 let layout = connection_layout();
398 let out = render_explain(&layout);
399 assert!(out.contains("│ 0 │"), "all fields must be on cache line 0");
401 assert!(
402 !out.contains("│ 1 │"),
403 "no field should be on cache line 1"
404 );
405 }
406
407 #[test]
408 fn explain_cl_column_shows_nonzero_for_large_struct() {
409 use padlock_core::arch::X86_64_SYSV;
410 use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
411 let big = StructLayout {
412 name: "Big".to_string(),
413 total_size: 128,
414 align: 8,
415 fields: vec![
416 Field {
417 name: "a".to_string(),
418 ty: TypeInfo::Primitive {
419 name: "u8[64]".to_string(),
420 size: 64,
421 align: 1,
422 },
423 offset: 0,
424 size: 64,
425 align: 1,
426 source_file: None,
427 source_line: None,
428 access: AccessPattern::Unknown,
429 },
430 Field {
431 name: "b".to_string(),
432 ty: TypeInfo::Primitive {
433 name: "u64".to_string(),
434 size: 8,
435 align: 8,
436 },
437 offset: 64,
438 size: 8,
439 align: 8,
440 source_file: None,
441 source_line: None,
442 access: AccessPattern::Unknown,
443 },
444 ],
445 source_file: None,
446 source_line: None,
447 arch: &X86_64_SYSV,
448 is_packed: false,
449 is_union: false,
450 is_repr_rust: false,
451 suppressed_findings: Vec::new(),
452 uncertain_fields: Vec::new(),
453 };
454 let out = render_explain(&big);
455 assert!(out.contains("│ 1 │"), "field b must show CL 1");
457 }
458
459 #[test]
460 fn explain_no_cache_line_separator_for_small_struct() {
461 let layout = connection_layout();
463 let out = render_explain(&layout);
464 assert!(
465 !out.contains("cache line 1"),
466 "single-cache-line struct must not show separator"
467 );
468 }
469
470 #[test]
471 fn fmt_count_formats_correctly() {
472 assert_eq!(fmt_count(999), "999");
473 assert_eq!(fmt_count(1_000), "1K");
474 assert_eq!(fmt_count(125_000), "125K");
475 assert_eq!(fmt_count(1_000_000), "1M");
476 assert_eq!(fmt_count(2_500_000), "2M");
477 }
478}