From 71556b76de1d3eba2a92e23eac2ba1ab5310b3c3 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Fri, 2 Oct 2026 08:35:47 +0100 Subject: [PATCH] Speed up snapshot loading; polish help and table bars Decompression and checksumming overlap with parsing via a pipe, and a concrete-typed varint reader removes per-byte interface calls (0.39s -> 0.29s for 458K nodes). --- PERFORMANCE.md | 16 +++++++++++ internal/snapshot/snapshot.go | 46 ++++++++++++++++++++++++++++-- internal/snapshot/snapshot_test.go | 16 +++++++++++ internal/tui/modals.go | 8 ++++-- internal/tui/table.go | 2 +- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/PERFORMANCE.md b/PERFORMANCE.md index bb98813..e34a09a 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -44,6 +44,22 @@ path reconstruction for job dispatch ~7 %; aggregation is negligible. **~158 bytes per node** (node record ~112 bytes plus name storage and directory side tables). Extrapolated, 10 M entries need ~1.6 GiB. +## Snapshots + +Same 458,510-node tree: + +| operation | result | +|---|---:| +| snapshot file size (gzip level 1) | 6.7 MB (~15 bytes/node) | +| scan + save | 1.03 s | +| load (`mapsize --no-ui file.msz`), 3 runs | 0.28 / 0.29 / 0.32 s, 81 MB RSS | +| warm-cache rescan for comparison | 0.55 s | + +Loading decompresses and verifies SHA-256 on a separate goroutine, overlapping +with parsing. Against a warm cache the win is ~1.9×; against a cold cache or +a network filesystem, where scans are dominated by I/O latency, it is far +larger (not yet measured here). + ## UI `go test ./internal/tui -bench Big` on a synthetic in-memory tree of diff --git a/internal/snapshot/snapshot.go b/internal/snapshot/snapshot.go index 4b5169d..52c1980 100644 --- a/internal/snapshot/snapshot.go +++ b/internal/snapshot/snapshot.go @@ -235,7 +235,7 @@ func (r *reader) uvarint(max uint64, what string) uint64 { if r.err != nil { return 0 } - v, err := binary.ReadUvarint(r.r) + v, err := readUvarint(r.r) if err != nil { r.fail("bad %s: %v", what, err) return 0 @@ -251,13 +251,44 @@ func (r *reader) varint(what string) int64 { if r.err != nil { return 0 } - v, err := binary.ReadVarint(r.r) + ux, err := readUvarint(r.r) if err != nil { r.fail("bad %s: %v", what, err) } + v := int64(ux >> 1) // zig-zag, as encoding/binary.PutVarint + if ux&1 != 0 { + v = ^v + } return v } +var errOverflow = errors.New("varint overflows 64 bits") + +// readUvarint is encoding/binary.ReadUvarint specialised to *bufio.Reader +// (no interface dispatch per byte; this is the loader's hot loop). +func readUvarint(br *bufio.Reader) (uint64, error) { + var x uint64 + var s uint + for i := 0; i < binary.MaxVarintLen64; i++ { + b, err := br.ReadByte() + if err != nil { + if i > 0 && err == io.EOF { + err = io.ErrUnexpectedEOF + } + return x, err + } + if b < 0x80 { + if i == binary.MaxVarintLen64-1 && b > 1 { + return x, errOverflow + } + return x | uint64(b)< 5GB ext IN (iso,qcow2) age > 365d", Style{FG: t.Muted})) out = append(out, styled(" path contains cache AND NOT type = dir owner = andy flag = sparse", Style{FG: t.Muted})) - out = append(out, styled(" fields: "+filter.Fields, Style{FG: t.Muted})) + fields := strings.Fields(filter.Fields) + out = append(out, styled(" fields: "+strings.Join(fields[:7], " "), Style{FG: t.Muted})) + out = append(out, styled(" "+strings.Join(fields[7:], " "), Style{FG: t.Muted})) out = append(out, styled(" sizes: KiB/MiB/GiB (1024), kB/MB/GB (1000); ages: s min h d w mo y", Style{FG: t.Muted})) sec("VIEWS") row("Tab Shift+Tab", "next / previous view") diff --git a/internal/tui/table.go b/internal/tui/table.go index f53263b..c155c65 100644 --- a/internal/tui/table.go +++ b/internal/tui/table.go @@ -126,7 +126,7 @@ func (tb *table) paint(m *Model, cv *Canvas, r treemap.Rect, cols []column, rows if !sel && ci < len(row.styles) && row.styles[ci] != nil { st = *row.styles[ci] } - if c.title == "" && row.bar >= 0 && c.width > 0 { + if c.title == "" && row.bar >= 0 { m.paintBar(cv, x, y, w, row.bar, row.barCol, sel) } else if c.right { cv.Text(x, y, textutil.PadLeft(s, w), w, st)