sonar

Scan files at memory bandwidth speed.
Log | Files | Refs

commit b351ce96c62247bdc5ed83dca067eac5db84433f
parent 385967ee523ba3745ae38971da391e94ed74053f
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Fri, 18 Sep 2026 08:01:49 -0400

main: report where scan time goes

Scan latency is now a product requirement, so the tool has to say which phase
owns it rather than leaving the answer to guesswork. The reader accumulates
time spent in volume reads separately from time spent folding records, and
main times the two aggregation passes. On a warm cache the split is roughly
two thirds reading and one fifth parsing, which points any optimisation work
at the read path first.

Diffstat:
Mmain.odin | 11+++++++++++
Mntfs/mft.odin | 4++++
Mntfs/reader.odin | 7+++++++
3 files changed, 22 insertions(+), 0 deletions(-)

diff --git a/main.odin b/main.odin @@ -76,8 +76,19 @@ run :: proc() -> int { } print_summary(&m, elapsed) + t_files := time.tick_now() print_largest_files(&m, prefix) + d_files := time.tick_since(t_files) + t_dirs := time.tick_now() print_largest_directories(&m, prefix) + d_dirs := time.tick_since(t_dirs) + fmt.println() + fmt.printfln("phase io %.0f ms", f64(m.stats.io_ns) / 1e6) + fmt.printfln("phase parse %.0f ms", f64(m.stats.parse_ns) / 1e6) + fmt.printfln("phase bitmap %.0f ms", f64(m.stats.bitmap_ns) / 1e6) + fmt.printfln("phase files %.0f ms", time.duration_milliseconds(d_files)) + fmt.printfln("phase dirs %.0f ms", time.duration_milliseconds(d_dirs)) + fmt.printfln("phase total %.0f ms", time.duration_milliseconds(time.tick_since(start))) return 0 } diff --git a/ntfs/mft.odin b/ntfs/mft.odin @@ -41,6 +41,10 @@ Mft_Stats :: struct { // Clusters $Bitmap reports as allocated; 0 when it could not be read. This is the // file system's own total, independent of the per-file sums. allocated_clusters: u64, + // Phase timings, nanoseconds. + io_ns: i64, + parse_ns: i64, + bitmap_ns: i64, } Mft :: struct { diff --git a/ntfs/reader.odin b/ntfs/reader.odin @@ -1,6 +1,7 @@ package ntfs import "core:mem" +import "core:time" // Alignment for I/O buffers. Raw volume reads want sector alignment; 4 KiB covers // every sector size in use. @@ -137,9 +138,13 @@ read_mft_from_volume :: proc(v: ^Volume, opts := Read_Options{}, allocator := co record_number: u32 = 0 for offset: u64 = 0; offset < mft_bytes; offset += u64(chunk) { n := int(min(u64(chunk), mft_bytes - offset)) + io_start := time.tick_now() if read_err := read_logical(&reader, offset, buf[:n]); read_err != nil { return {}, read_err } + m.stats.io_ns += i64(time.tick_since(io_start)) + parse_start := time.tick_now() + defer m.stats.parse_ns += i64(time.tick_since(parse_start)) for start := 0; start + record_size <= n; start += record_size { rec := buf[start:start + record_size] defer record_number += 1 @@ -159,9 +164,11 @@ read_mft_from_volume :: proc(v: ^Volume, opts := Read_Options{}, allocator := co } // $Bitmap is the file system's own count of used clusters, which checks the sums // built above. Failing to read it costs nothing else, so the table still stands. + bitmap_start := time.tick_now() if c, bitmap_err := read_bitmap_clusters(&reader, boot, buf, allocator); bitmap_err == nil { m.stats.allocated_clusters = c } + m.stats.bitmap_ns = i64(time.tick_since(bitmap_start)) ok = true return m, .None