1use std::collections::HashMap;
8use std::io::Write;
9use std::time::Instant;
10
11use crate::cli::wprintln;
12use crate::innodb::health;
13use crate::innodb::record::walk_compact_records;
14use crate::innodb::sdi;
15use crate::util::prometheus as prom;
16use crate::IdbError;
17
18pub struct HealthOptions {
20 pub file: String,
22 pub verbose: bool,
24 pub json: bool,
26 pub csv: bool,
28 pub prometheus: bool,
30 pub bloat: bool,
32 pub cardinality: bool,
34 pub sample_size: usize,
36 pub page_size: Option<u32>,
38 pub keyring: Option<String>,
40 pub mmap: bool,
42}
43
44pub fn execute(opts: &HealthOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
46 if opts.prometheus && (opts.json || opts.csv) {
47 return Err(IdbError::Argument(
48 "--prometheus cannot be combined with JSON or CSV output".to_string(),
49 ));
50 }
51
52 let start = Instant::now();
53
54 let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
55
56 if let Some(ref keyring_path) = opts.keyring {
57 crate::cli::setup_decryption(&mut ts, keyring_path)?;
58 }
59
60 let page_size = ts.page_size();
61 let total_file_pages = ts.page_count();
62
63 let mut snapshots = Vec::new();
65 let mut empty_pages = 0u64;
66 let mut rtree_pages = 0u64;
67 let mut lob_pages = 0u64;
68 let mut undo_pages = 0u64;
69
70 let mut delete_counts: HashMap<u64, (u64, u64)> = HashMap::new();
72 let mut leaf_pages_by_index: HashMap<u64, Vec<u64>> = HashMap::new();
74
75 ts.for_each_page(|page_num, data| {
76 if data.iter().all(|&b| b == 0) {
77 empty_pages += 1;
78 } else if let Some(snap) = health::extract_index_page_snapshot(data, page_num) {
79 if snap.level == 0 && opts.cardinality {
81 leaf_pages_by_index
82 .entry(snap.index_id)
83 .or_default()
84 .push(snap.page_number);
85 }
86
87 if snap.level == 0 && opts.bloat {
89 let recs = walk_compact_records(data);
90 let total = recs.len() as u64;
91 let deleted = recs.iter().filter(|r| r.header.delete_mark()).count() as u64;
92 let entry = delete_counts.entry(snap.index_id).or_insert((0, 0));
93 entry.0 += deleted;
94 entry.1 += total;
95 }
96
97 snapshots.push(snap);
98 }
99
100 if let Some(fil) = crate::innodb::page::FilHeader::parse(data) {
102 use crate::innodb::page_types::PageType;
103 match fil.page_type {
104 PageType::Rtree | PageType::EncryptedRtree => rtree_pages += 1,
105 PageType::Blob
106 | PageType::ZBlob
107 | PageType::ZBlob2
108 | PageType::LobFirst
109 | PageType::LobData
110 | PageType::LobIndex
111 | PageType::ZlobFirst
112 | PageType::ZlobData
113 | PageType::ZlobFrag
114 | PageType::ZlobFragEntry
115 | PageType::ZlobIndex => lob_pages += 1,
116 PageType::UndoLog => undo_pages += 1,
117 _ => {}
118 }
119 }
120 Ok(())
121 })?;
122
123 let mut report = health::analyze_health(
124 snapshots,
125 page_size,
126 total_file_pages,
127 empty_pages,
128 &opts.file,
129 );
130 report.summary.rtree_pages = rtree_pages;
131 report.summary.lob_pages = lob_pages;
132 report.summary.undo_pages = undo_pages;
133
134 resolve_index_names(
136 &opts.file,
137 opts.page_size,
138 opts.mmap,
139 &opts.keyring,
140 &mut report,
141 );
142
143 if opts.bloat {
145 for idx in &mut report.indexes {
146 let (deleted, total) = delete_counts.get(&idx.index_id).copied().unwrap_or((0, 0));
147 let delete_mark_ratio = if total > 0 {
148 deleted as f64 / total as f64
149 } else {
150 0.0
151 };
152 idx.delete_marked_records = Some(deleted);
153 idx.total_walked_records = Some(total);
154 idx.bloat = Some(health::score_bloat(idx, delete_mark_ratio));
155 }
156 }
157
158 if opts.cardinality {
163 let columns_opt = crate::innodb::export::extract_column_layout(&mut ts);
164 if let Some((columns, clustered_index_id)) = columns_opt {
165 let col_name = columns.first().map(|c| c.name.clone()).unwrap_or_default();
166 if let Some(idx) = report
167 .indexes
168 .iter_mut()
169 .find(|i| i.index_id == clustered_index_id)
170 {
171 if let Some(leaf_pages) = leaf_pages_by_index.get(&idx.index_id) {
172 idx.cardinality = health::estimate_cardinality(
173 &mut ts,
174 leaf_pages,
175 &columns,
176 &col_name,
177 page_size,
178 opts.sample_size,
179 );
180 }
181 }
182 }
183 }
184
185 let duration_secs = start.elapsed().as_secs_f64();
186
187 if opts.prometheus {
188 print_prometheus(writer, &report, duration_secs)?;
189 return Ok(());
190 }
191
192 if opts.json {
193 wprintln!(
194 writer,
195 "{}",
196 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?
197 )?;
198 } else if opts.csv {
199 print_csv(writer, &report, opts.bloat, opts.cardinality)?;
200 } else {
201 print_text(writer, &report, opts.verbose)?;
202 }
203
204 Ok(())
205}
206
207fn resolve_index_names(
209 file: &str,
210 page_size: Option<u32>,
211 mmap: bool,
212 keyring: &Option<String>,
213 report: &mut health::HealthReport,
214) {
215 let resolve = || -> Result<std::collections::HashMap<u64, String>, IdbError> {
216 let mut ts = crate::cli::open_tablespace(file, page_size, mmap)?;
217 if let Some(ref kp) = keyring {
218 crate::cli::setup_decryption(&mut ts, kp)?;
219 }
220 let sdi_pages = sdi::find_sdi_pages(&mut ts)?;
221 if sdi_pages.is_empty() {
222 return Ok(std::collections::HashMap::new());
223 }
224 let records = sdi::extract_sdi_from_pages(&mut ts, &sdi_pages)?;
225 let mut name_map = std::collections::HashMap::new();
226 for rec in &records {
227 if rec.sdi_type == 1 {
228 name_map.extend(sdi::build_index_name_map(&rec.data));
229 }
230 }
231 Ok(name_map)
232 };
233
234 if let Ok(name_map) = resolve() {
235 for idx in &mut report.indexes {
236 if let Some(name) = name_map.get(&idx.index_id) {
237 idx.index_name = Some(name.clone());
238 }
239 }
240 }
241}
242
243fn print_text(
248 writer: &mut dyn Write,
249 report: &health::HealthReport,
250 verbose: bool,
251) -> Result<(), IdbError> {
252 wprintln!(writer, "Tablespace Health Report: {}", report.file)?;
253 wprintln!(writer)?;
254
255 if report.indexes.is_empty() {
256 wprintln!(writer, "No INDEX pages found.")?;
257 wprintln!(writer)?;
258 }
259
260 for idx in &report.indexes {
261 let name = idx.index_name.as_deref().unwrap_or("(unknown)");
262 wprintln!(writer, "Index {} ({}):", idx.index_id, name)?;
263 wprintln!(writer, " Tree depth: {}", idx.tree_depth)?;
264 wprintln!(
265 writer,
266 " Pages: {} total ({} leaf, {} non-leaf)",
267 idx.total_pages,
268 idx.leaf_pages,
269 idx.non_leaf_pages
270 )?;
271 wprintln!(
272 writer,
273 " Fill factor: avg {:.0}%, min {:.0}%, max {:.0}%",
274 idx.avg_fill_factor * 100.0,
275 idx.min_fill_factor * 100.0,
276 idx.max_fill_factor * 100.0
277 )?;
278 wprintln!(
279 writer,
280 " Garbage: {:.1}% ({} bytes)",
281 idx.avg_garbage_ratio * 100.0,
282 idx.total_garbage_bytes
283 )?;
284 wprintln!(
285 writer,
286 " Fragmentation: {:.1}%",
287 idx.fragmentation * 100.0
288 )?;
289
290 if verbose {
291 wprintln!(writer, " Total records: {}", idx.total_records)?;
292 if idx.empty_leaf_pages > 0 {
293 wprintln!(writer, " Empty leaves: {}", idx.empty_leaf_pages)?;
294 }
295 }
296
297 if let Some(ref bloat) = idx.bloat {
299 wprintln!(
300 writer,
301 " Bloat: {} ({:.2})",
302 bloat.grade,
303 bloat.score
304 )?;
305 if let Some(ref rec) = bloat.recommendation {
306 wprintln!(writer, " {}", rec)?;
307 }
308 }
309
310 if let Some(ref card) = idx.cardinality {
312 wprintln!(
313 writer,
314 " Cardinality: ~{} distinct (column: {}, {}/{} pages, {:.0}% confidence)",
315 card.estimated_distinct,
316 card.column_name,
317 card.sampled_pages,
318 card.total_leaf_pages,
319 card.confidence * 100.0
320 )?;
321 }
322
323 wprintln!(writer)?;
324 }
325
326 wprintln!(writer, "Summary:")?;
328 wprintln!(
329 writer,
330 " Total pages: {} ({} INDEX, {} non-INDEX, {} empty)",
331 report.summary.total_pages,
332 report.summary.index_pages,
333 report.summary.non_index_pages,
334 report.summary.empty_pages
335 )?;
336 wprintln!(
337 writer,
338 " Page size: {} bytes",
339 report.summary.page_size
340 )?;
341 wprintln!(writer, " Indexes: {}", report.summary.index_count)?;
342 if report.summary.rtree_pages > 0 {
343 wprintln!(writer, " RTREE pages: {}", report.summary.rtree_pages)?;
344 }
345 if report.summary.lob_pages > 0 {
346 wprintln!(writer, " LOB/BLOB pages: {}", report.summary.lob_pages)?;
347 }
348 if report.summary.undo_pages > 0 {
349 wprintln!(writer, " UNDO pages: {}", report.summary.undo_pages)?;
350 }
351 wprintln!(
352 writer,
353 " Avg fill factor: {:.0}%",
354 report.summary.avg_fill_factor * 100.0
355 )?;
356 wprintln!(
357 writer,
358 " Avg garbage: {:.1}%",
359 report.summary.avg_garbage_ratio * 100.0
360 )?;
361 wprintln!(
362 writer,
363 " Avg fragmentation: {:.1}%",
364 report.summary.avg_fragmentation * 100.0
365 )?;
366
367 Ok(())
368}
369
370fn print_csv(
375 writer: &mut dyn Write,
376 report: &health::HealthReport,
377 bloat: bool,
378 cardinality: bool,
379) -> Result<(), IdbError> {
380 let mut header = String::from(
381 "index_id,index_name,tree_depth,total_pages,leaf_pages,avg_fill_factor,garbage_ratio,fragmentation",
382 );
383 if bloat {
384 header.push_str(",bloat_score,bloat_grade,delete_marked,total_walked");
385 }
386 if cardinality {
387 header.push_str(",est_cardinality,cardinality_confidence");
388 }
389 wprintln!(writer, "{}", header)?;
390
391 for idx in &report.indexes {
392 let mut row = format!(
393 "{},{},{},{},{},{},{},{}",
394 idx.index_id,
395 crate::cli::csv_escape(idx.index_name.as_deref().unwrap_or("")),
396 idx.tree_depth,
397 idx.total_pages,
398 idx.leaf_pages,
399 idx.avg_fill_factor,
400 idx.avg_garbage_ratio,
401 idx.fragmentation
402 );
403 if bloat {
404 if let Some(ref b) = idx.bloat {
405 row.push_str(&format!(
406 ",{},{},{}",
407 b.score,
408 b.grade,
409 idx.delete_marked_records.unwrap_or(0)
410 ));
411 row.push_str(&format!(",{}", idx.total_walked_records.unwrap_or(0)));
412 } else {
413 row.push_str(",,,,");
414 }
415 }
416 if cardinality {
417 if let Some(ref c) = idx.cardinality {
418 row.push_str(&format!(",{},{}", c.estimated_distinct, c.confidence));
419 } else {
420 row.push_str(",,");
421 }
422 }
423 wprintln!(writer, "{}", row)?;
424 }
425 Ok(())
426}
427
428fn print_prometheus(
433 writer: &mut dyn Write,
434 report: &health::HealthReport,
435 duration_secs: f64,
436) -> Result<(), IdbError> {
437 let file = &report.file;
438
439 wprintln!(
441 writer,
442 "{}",
443 prom::help_line("innodb_pages", "Total pages in the tablespace by type")
444 )?;
445 wprintln!(writer, "{}", prom::type_line("innodb_pages", "gauge"))?;
446 wprintln!(
447 writer,
448 "{}",
449 prom::format_gauge_int(
450 "innodb_pages",
451 &[("file", file), ("type", "index")],
452 report.summary.index_pages
453 )
454 )?;
455 wprintln!(
456 writer,
457 "{}",
458 prom::format_gauge_int(
459 "innodb_pages",
460 &[("file", file), ("type", "non_index")],
461 report.summary.non_index_pages
462 )
463 )?;
464 wprintln!(
465 writer,
466 "{}",
467 prom::format_gauge_int(
468 "innodb_pages",
469 &[("file", file), ("type", "empty")],
470 report.summary.empty_pages
471 )
472 )?;
473
474 wprintln!(
476 writer,
477 "{}",
478 prom::help_line("innodb_fill_factor", "Average B+Tree fill factor per index")
479 )?;
480 wprintln!(writer, "{}", prom::type_line("innodb_fill_factor", "gauge"))?;
481 for idx in &report.indexes {
482 let id_str = idx.index_id.to_string();
483 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
484 wprintln!(
485 writer,
486 "{}",
487 prom::format_gauge(
488 "innodb_fill_factor",
489 &[("file", file), ("index", index_label)],
490 idx.avg_fill_factor
491 )
492 )?;
493 }
494
495 wprintln!(
497 writer,
498 "{}",
499 prom::help_line(
500 "innodb_fragmentation_ratio",
501 "Leaf-level fragmentation ratio per index"
502 )
503 )?;
504 wprintln!(
505 writer,
506 "{}",
507 prom::type_line("innodb_fragmentation_ratio", "gauge")
508 )?;
509 for idx in &report.indexes {
510 let id_str = idx.index_id.to_string();
511 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
512 wprintln!(
513 writer,
514 "{}",
515 prom::format_gauge(
516 "innodb_fragmentation_ratio",
517 &[("file", file), ("index", index_label)],
518 idx.fragmentation
519 )
520 )?;
521 }
522
523 wprintln!(
525 writer,
526 "{}",
527 prom::help_line("innodb_garbage_ratio", "Average garbage ratio per index")
528 )?;
529 wprintln!(
530 writer,
531 "{}",
532 prom::type_line("innodb_garbage_ratio", "gauge")
533 )?;
534 for idx in &report.indexes {
535 let id_str = idx.index_id.to_string();
536 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
537 wprintln!(
538 writer,
539 "{}",
540 prom::format_gauge(
541 "innodb_garbage_ratio",
542 &[("file", file), ("index", index_label)],
543 idx.avg_garbage_ratio
544 )
545 )?;
546 }
547
548 wprintln!(
550 writer,
551 "{}",
552 prom::help_line("innodb_index_pages", "Total pages per index")
553 )?;
554 wprintln!(writer, "{}", prom::type_line("innodb_index_pages", "gauge"))?;
555 for idx in &report.indexes {
556 let id_str = idx.index_id.to_string();
557 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
558 wprintln!(
559 writer,
560 "{}",
561 prom::format_gauge_int(
562 "innodb_index_pages",
563 &[("file", file), ("index", index_label)],
564 idx.total_pages
565 )
566 )?;
567 }
568
569 let has_bloat = report.indexes.iter().any(|i| i.bloat.is_some());
571 if has_bloat {
572 wprintln!(
573 writer,
574 "{}",
575 prom::help_line("innodb_bloat_score", "Index bloat score (0.0-1.0)")
576 )?;
577 wprintln!(writer, "{}", prom::type_line("innodb_bloat_score", "gauge"))?;
578 for idx in &report.indexes {
579 if let Some(ref b) = idx.bloat {
580 let id_str = idx.index_id.to_string();
581 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
582 wprintln!(
583 writer,
584 "{}",
585 prom::format_gauge(
586 "innodb_bloat_score",
587 &[("file", file), ("index", index_label)],
588 b.score
589 )
590 )?;
591 }
592 }
593
594 wprintln!(
595 writer,
596 "{}",
597 prom::help_line(
598 "innodb_delete_mark_ratio",
599 "Ratio of delete-marked records per index"
600 )
601 )?;
602 wprintln!(
603 writer,
604 "{}",
605 prom::type_line("innodb_delete_mark_ratio", "gauge")
606 )?;
607 for idx in &report.indexes {
608 if let Some(ref b) = idx.bloat {
609 let id_str = idx.index_id.to_string();
610 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
611 wprintln!(
612 writer,
613 "{}",
614 prom::format_gauge(
615 "innodb_delete_mark_ratio",
616 &[("file", file), ("index", index_label)],
617 b.components.delete_mark_ratio
618 )
619 )?;
620 }
621 }
622 }
623
624 wprintln!(
626 writer,
627 "{}",
628 prom::help_line(
629 "innodb_scan_duration_seconds",
630 "Time spent scanning the tablespace"
631 )
632 )?;
633 wprintln!(
634 writer,
635 "{}",
636 prom::type_line("innodb_scan_duration_seconds", "gauge")
637 )?;
638 wprintln!(
639 writer,
640 "{}",
641 prom::format_gauge(
642 "innodb_scan_duration_seconds",
643 &[("file", file)],
644 duration_secs
645 )
646 )?;
647
648 Ok(())
649}