Skip to content

perf: eliminate O(N²) sort cost and reduce viewer search allocations - #96

Merged
deveshctl merged 3 commits into
mainfrom
feat/tui-sort-search-perf
Aug 5, 2026
Merged

perf: eliminate O(N²) sort cost and reduce viewer search allocations#96
deveshctl merged 3 commits into
mainfrom
feat/tui-sort-search-perf

Conversation

@deveshctl

Copy link
Copy Markdown
Owner

Summary

  • Directory sizes used by sort-by-size are now computed once when a layer's file tree is built (post-order DFS populating FileNode.EffectiveSize), rather than re-walking each directory's subtree on every sort invocation. For large layers with deep directory trees, sort cost drops from O(N²) to O(1) per node after the one-time build pass.

  • Incremental search in the file viewer no longer allocates a temporary string per candidate position when scanning each visible line for matches. The inner scan now uses strings.Index on the lowercased line tail, with a single utf8.RuneCountInString call to convert the confirmed byte offset back to a rune offset. Reduces allocation pressure during active search through large files.

Changes

  • image/filetree.go — added FileNode.EffectiveSize field and computeEffectiveSizes() post-order DFS
  • image/stack.go — call computeEffectiveSizes() on every tree produced by Stack and BuildAggregatedTrees
  • tui/filetree.goapplySortBySize and the size-column path read EffectiveSize directly; nodeEffectiveSize retained as tested reference
  • tui/fileview.go — replaced allocating inner match-scan loop in renderViewerLine with strings.Index
  • image/filetree_test.go — 7 new tests covering computeEffectiveSizes edge cases

Test plan

  • go build ./... and go vet ./... clean
  • CI passes (go test ./... including new image/filetree_test.go tests)
  • On a real image with a large layer: toggle sort (s) while navigating — held-key scroll stays responsive
  • Open a large file in the viewer, activate search (/), type a query — no visible lag while scanning results

The inner loop in renderViewerLine converted rune slices to strings on
every candidate position to find search matches, producing O(N·W)
temporary allocations per visible line (N lines × W candidate positions).
Replaced with strings.Index on the lowercased tail of the line, which
finds each match in O(N) without per-position allocation. The byte offset
returned by strings.Index is converted back to a rune offset once per
confirmed match via utf8.RuneCountInString.
…time

nodeEffectiveSize performed a full recursive subtree walk for every
directory in the visible list on each sort invocation, making sort cost
O(N²) in node count for large layers with deep directory trees.

Added FileNode.EffectiveSize, populated by a single post-order DFS
(computeEffectiveSizes) called once per tree after it is fully built in
Stack and BuildAggregatedTrees. The TUI sort and size-column paths read
the field directly, turning per-sort subtree walks into O(1) field reads.
gofmt splits the struct's field-alignment block at the EffectiveSize doc
comment; align the pre-comment fields to their own width so gofmt is
clean. No behaviour change.
@deveshctl
deveshctl merged commit 2143e71 into main Aug 5, 2026
24 checks passed
@deveshctl
deveshctl deleted the feat/tui-sort-search-perf branch August 7, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant