4141use std:: io:: { BufRead , BufReader } ;
4242use std:: path:: { Path , PathBuf } ;
4343use std:: process:: Command ;
44+ use std:: time:: { Duration , Instant } ;
4445use std:: { env, fs} ;
4546
4647use sha2:: { Digest , Sha256 } ;
@@ -59,6 +60,16 @@ struct DriveResult {
5960 result : VerifyResult ,
6061 baseline_lines : usize ,
6162 rust_lines : usize ,
63+ mft_size_bytes : u64 ,
64+ parse_duration : Option < Duration > ,
65+ }
66+
67+ /// Result from running uffs to regenerate output
68+ #[ derive( Debug ) ]
69+ struct RegenerateResult {
70+ output_path : PathBuf ,
71+ parse_duration : Duration ,
72+ mft_size_bytes : u64 ,
6273}
6374
6475#[ derive( Debug ) ]
@@ -115,26 +126,27 @@ fn run_legacy_mode(args: &[String], base_dir: &Path) {
115126
116127 // Determine mode
117128 let mode = & args[ 3 ] ;
118- let rust_output = match mode. as_str ( ) {
129+ let ( rust_output, parse_duration , mft_size ) = match mode. as_str ( ) {
119130 "--regenerate" => {
120131 let golden_baseline = find_golden_baseline_file ( & drive_dir, & drive_lower) ;
121132 let tz_offset =
122133 explicit_tz. unwrap_or_else ( || detect_tz_from_baseline ( & golden_baseline) ) ;
123- regenerate_rust_output (
134+ let regen = regenerate_rust_output (
124135 & drive_dir,
125136 & drive_letter,
126137 & drive_lower,
127138 tz_offset,
128139 custom_bin. as_deref ( ) ,
129- )
140+ ) ;
141+ ( regen. output_path , Some ( regen. parse_duration ) , regen. mft_size_bytes )
130142 }
131143 "--rust" => {
132144 if args. len ( ) < 5 {
133145 eprintln ! ( "ERROR: --rust requires a path argument" ) ;
134146 print_usage ( & args[ 0 ] ) ;
135147 std:: process:: exit ( 1 ) ;
136148 }
137- PathBuf :: from ( & args[ 4 ] )
149+ ( PathBuf :: from ( & args[ 4 ] ) , None , 0 )
138150 }
139151 _ => {
140152 eprintln ! ( "ERROR: Unknown mode: {}" , mode) ;
@@ -143,7 +155,19 @@ fn run_legacy_mode(args: &[String], base_dir: &Path) {
143155 }
144156 } ;
145157
146- let result = verify_single_drive ( base_dir, & drive_dir, & drive_letter, & rust_output) ;
158+ let result = verify_single_drive ( base_dir, & drive_dir, & drive_letter, & rust_output, parse_duration, mft_size) ;
159+
160+ // Print timing for single drive if available
161+ if let Some ( duration) = result. parse_duration {
162+ println ! ( ) ;
163+ println ! ( "⏱️ MFT Parse Time: {}" , format_duration( duration) ) ;
164+ if result. mft_size_bytes > 0 {
165+ let mb = result. mft_size_bytes as f64 / ( 1024.0 * 1024.0 ) ;
166+ let throughput = mb / duration. as_secs_f64 ( ) ;
167+ println ! ( " MFT Size: {:.1} MB, Throughput: {:.1} MB/s" , mb, throughput) ;
168+ }
169+ }
170+
147171 std:: process:: exit ( if result. result == VerifyResult :: Mismatch {
148172 1
149173 } else {
@@ -217,27 +241,30 @@ fn run_multi_drive_mode(args: &[String], base_dir: &Path) {
217241 result : VerifyResult :: Skipped ,
218242 baseline_lines : 0 ,
219243 rust_lines : 0 ,
244+ mft_size_bytes : 0 ,
245+ parse_duration : None ,
220246 } ) ;
221247 continue ;
222248 }
223249 } ;
224250
225251 // Generate or use provided rust output
226- let rust_output = if let Some ( ref path) = rust_output_path {
227- PathBuf :: from ( path)
252+ let ( rust_output, parse_duration , mft_size ) = if let Some ( ref path) = rust_output_path {
253+ ( PathBuf :: from ( path) , None , 0u64 )
228254 } else {
229255 let tz_offset =
230256 explicit_tz. unwrap_or_else ( || detect_tz_from_baseline ( & golden_baseline) ) ;
231- regenerate_rust_output (
257+ let regen = regenerate_rust_output (
232258 & drive_dir,
233259 & drive_letter,
234260 drive_lower,
235261 tz_offset,
236262 custom_bin. as_deref ( ) ,
237- )
263+ ) ;
264+ ( regen. output_path , Some ( regen. parse_duration ) , regen. mft_size_bytes )
238265 } ;
239266
240- let result = verify_single_drive ( base_dir, & drive_dir, & drive_letter, & rust_output) ;
267+ let result = verify_single_drive ( base_dir, & drive_dir, & drive_letter, & rust_output, parse_duration , mft_size ) ;
241268 results. push ( result) ;
242269 println ! ( ) ;
243270 }
@@ -294,6 +321,8 @@ fn verify_single_drive(
294321 drive_dir : & Path ,
295322 drive_letter : & str ,
296323 rust_output : & Path ,
324+ parse_duration : Option < Duration > ,
325+ mft_size_bytes : u64 ,
297326) -> DriveResult {
298327 let drive_lower = drive_letter. to_lowercase ( ) ;
299328 let golden_baseline_file = find_golden_baseline_file ( drive_dir, & drive_lower) ;
@@ -308,6 +337,8 @@ fn verify_single_drive(
308337 result : VerifyResult :: Mismatch ,
309338 baseline_lines : 0 ,
310339 rust_lines : 0 ,
340+ mft_size_bytes,
341+ parse_duration,
311342 } ;
312343 }
313344
@@ -341,6 +372,8 @@ fn verify_single_drive(
341372 result : VerifyResult :: StrictMatch ,
342373 baseline_lines : golden_hashes. line_count ,
343374 rust_lines : rust_hashes. line_count ,
375+ mft_size_bytes,
376+ parse_duration,
344377 } ;
345378 }
346379
@@ -361,6 +394,8 @@ fn verify_single_drive(
361394 result : VerifyResult :: SortedMatch ,
362395 baseline_lines : golden_hashes. line_count ,
363396 rust_lines : rust_hashes. line_count ,
397+ mft_size_bytes,
398+ parse_duration,
364399 } ;
365400 }
366401
@@ -384,6 +419,8 @@ fn verify_single_drive(
384419 result : VerifyResult :: Mismatch ,
385420 baseline_lines : golden_hashes. line_count ,
386421 rust_lines : rust_hashes. line_count ,
422+ mft_size_bytes,
423+ parse_duration,
387424 }
388425}
389426
@@ -440,9 +477,84 @@ fn print_summary(results: &[DriveResult]) {
440477 } else {
441478 println ! ( " ⚠️ {} drive(s) had mismatches" , mismatch) ;
442479 }
480+
481+ // Print timing table if any drives have timing data
482+ let timed_results: Vec < _ > = results. iter ( ) . filter ( |r| r. parse_duration . is_some ( ) ) . collect ( ) ;
483+ if !timed_results. is_empty ( ) {
484+ print_timing_table ( & timed_results) ;
485+ }
443486 println ! ( ) ;
444487}
445488
489+ /// Print a timing table for MFT parsing performance
490+ fn print_timing_table ( results : & [ & DriveResult ] ) {
491+ println ! ( ) ;
492+ println ! ( "╔══════════════════════════════════════════════════════════════════════════════╗" ) ;
493+ println ! ( "║ MFT PARSING PERFORMANCE ║" ) ;
494+ println ! ( "╠═══════════╦══════════════╦═══════════════╦═══════════════╦══════════════════╣" ) ;
495+ println ! ( "║ Drive ║ MFT Size ║ Parse Time ║ Throughput ║ Files/sec ║" ) ;
496+ println ! ( "╠═══════════╬══════════════╬═══════════════╬═══════════════╬══════════════════╣" ) ;
497+
498+ let mut total_bytes: u64 = 0 ;
499+ let mut total_duration = Duration :: ZERO ;
500+ let mut total_files: usize = 0 ;
501+
502+ for result in results {
503+ if let Some ( duration) = result. parse_duration {
504+ let mft_mb = result. mft_size_bytes as f64 / ( 1024.0 * 1024.0 ) ;
505+ let secs = duration. as_secs_f64 ( ) ;
506+ let throughput_mb = if secs > 0.0 { mft_mb / secs } else { 0.0 } ;
507+ let files_per_sec = if secs > 0.0 { result. rust_lines as f64 / secs } else { 0.0 } ;
508+
509+ total_bytes += result. mft_size_bytes ;
510+ total_duration += duration;
511+ total_files += result. rust_lines ;
512+
513+ println ! (
514+ "║ {} ║ {:>10.1} MB ║ {:>13} ║ {:>10.1} MB/s ║ {:>14.0}/s ║" ,
515+ result. drive_letter,
516+ mft_mb,
517+ format_duration( duration) ,
518+ throughput_mb,
519+ files_per_sec
520+ ) ;
521+ }
522+ }
523+
524+ // Print totals
525+ if results. len ( ) > 1 {
526+ println ! ( "╠═══════════╬══════════════╬═══════════════╬═══════════════╬══════════════════╣" ) ;
527+ let total_mb = total_bytes as f64 / ( 1024.0 * 1024.0 ) ;
528+ let total_secs = total_duration. as_secs_f64 ( ) ;
529+ let avg_throughput = if total_secs > 0.0 { total_mb / total_secs } else { 0.0 } ;
530+ let avg_files_per_sec = if total_secs > 0.0 { total_files as f64 / total_secs } else { 0.0 } ;
531+
532+ println ! (
533+ "║ TOTAL ║ {:>10.1} MB ║ {:>13} ║ {:>10.1} MB/s ║ {:>14.0}/s ║" ,
534+ total_mb,
535+ format_duration( total_duration) ,
536+ avg_throughput,
537+ avg_files_per_sec
538+ ) ;
539+ }
540+
541+ println ! ( "╚═══════════╩══════════════╩═══════════════╩═══════════════╩══════════════════╝" ) ;
542+ }
543+
544+ /// Format a Duration as a human-readable string
545+ fn format_duration ( duration : Duration ) -> String {
546+ let total_secs = duration. as_secs_f64 ( ) ;
547+ if total_secs < 1.0 {
548+ format ! ( "{:.0}ms" , duration. as_millis( ) )
549+ } else if total_secs < 60.0 {
550+ format ! ( "{:.2}s" , total_secs)
551+ } else {
552+ let mins = ( total_secs / 60.0 ) . floor ( ) as u64 ;
553+ let secs = total_secs % 60.0 ;
554+ format ! ( "{}m {:.1}s" , mins, secs)
555+ }
556+ }
557+
446558/// Resolves the drive data directory.
447559///
448560/// Supports two directory structures:
@@ -805,7 +917,7 @@ fn regenerate_rust_output(
805917 drive_lower : & str ,
806918 tz_offset : i32 ,
807919 custom_bin : Option < & Path > ,
808- ) -> PathBuf {
920+ ) -> RegenerateResult {
809921 let tz_str = format ! ( "{}" , tz_offset) ;
810922 let tz_label = match tz_offset {
811923 -7 => "PDT (Pacific Daylight)" ,
@@ -826,7 +938,14 @@ fn regenerate_rust_output(
826938 eprintln ! ( "ERROR: MFT file not found: {}" , mft_file. display( ) ) ;
827939 std:: process:: exit ( 1 ) ;
828940 }
829- println ! ( "MFT file: {}" , mft_file. display( ) ) ;
941+
942+ // Get MFT file size
943+ let mft_size_bytes = fs:: metadata ( & mft_file)
944+ . map ( |m| m. len ( ) )
945+ . unwrap_or ( 0 ) ;
946+ let mft_mb = mft_size_bytes as f64 / ( 1024.0 * 1024.0 ) ;
947+
948+ println ! ( "MFT file: {} ({:.1} MB)" , mft_file. display( ) , mft_mb) ;
830949
831950 // Determine which binary to use
832951 let binary_path = if let Some ( custom) = custom_bin {
@@ -863,6 +982,9 @@ fn regenerate_rust_output(
863982 let rust_output = data_dir. join ( format ! ( "verify_rust_{}.txt" , drive_lower) ) ;
864983 println ! ( "Running uffs scan (baseline-compatible algorithms)..." ) ;
865984
985+ // Time the uffs execution
986+ let start_time = Instant :: now ( ) ;
987+
866988 let status = Command :: new ( & binary_path)
867989 . args ( [
868990 "*" ,
@@ -877,9 +999,16 @@ fn regenerate_rust_output(
877999 ] )
8781000 . status ( ) ;
8791001
1002+ let parse_duration = start_time. elapsed ( ) ;
1003+
8801004 match status {
8811005 Ok ( s) if s. success ( ) => {
882- println ! ( " uffs scan completed successfully." ) ;
1006+ let throughput = mft_mb / parse_duration. as_secs_f64 ( ) ;
1007+ println ! (
1008+ " ✅ uffs scan completed in {} ({:.1} MB/s)" ,
1009+ format_duration( parse_duration) ,
1010+ throughput
1011+ ) ;
8831012 println ! ( ) ;
8841013 }
8851014 Ok ( s) => {
@@ -892,7 +1021,11 @@ fn regenerate_rust_output(
8921021 }
8931022 }
8941023
895- rust_output
1024+ RegenerateResult {
1025+ output_path : rust_output,
1026+ parse_duration,
1027+ mft_size_bytes,
1028+ }
8961029}
8971030
8981031/// Find the authoritative workspace release artifact via Cargo metadata.
0 commit comments