Append-only chunked node arena with stable NodeIDs, intrusive child lists, per-directory category totals and batched ancestor propagation.
177 lines
3.8 KiB
Go
177 lines
3.8 KiB
Go
package inventory
|
|
|
|
import (
|
|
"container/heap"
|
|
"sort"
|
|
)
|
|
|
|
// SizeMode selects which size drives layout and sorting.
|
|
type SizeMode uint8
|
|
|
|
const (
|
|
SizeAllocated SizeMode = iota // disk usage (default)
|
|
SizeLogical // apparent size
|
|
)
|
|
|
|
func (m SizeMode) String() string {
|
|
if m == SizeLogical {
|
|
return "logical"
|
|
}
|
|
return "allocated"
|
|
}
|
|
|
|
// Total returns the node's aggregate size in the given mode.
|
|
func (n *Node) Total(m SizeMode) int64 {
|
|
if m == SizeLogical {
|
|
return n.TotSize
|
|
}
|
|
return n.TotAlloc
|
|
}
|
|
|
|
// Own returns the node's own size in the given mode.
|
|
func (n *Node) Own(m SizeMode) int64 {
|
|
if m == SizeLogical {
|
|
return n.Size
|
|
}
|
|
return n.Alloc
|
|
}
|
|
|
|
type ranked struct {
|
|
id NodeID
|
|
key int64
|
|
}
|
|
|
|
type minHeap []ranked
|
|
|
|
func (h minHeap) Len() int { return len(h) }
|
|
func (h minHeap) Less(i, j int) bool {
|
|
if h[i].key != h[j].key {
|
|
return h[i].key < h[j].key
|
|
}
|
|
return h[i].id > h[j].id
|
|
}
|
|
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
func (h *minHeap) Push(x any) { *h = append(*h, x.(ranked)) }
|
|
func (h *minHeap) Pop() any {
|
|
old := *h
|
|
x := old[len(old)-1]
|
|
*h = old[:len(old)-1]
|
|
return x
|
|
}
|
|
|
|
// TopK keeps the k entries with the largest keys; O(n log k).
|
|
type TopK struct {
|
|
k int
|
|
h minHeap
|
|
}
|
|
|
|
// NewTopK creates a collector for the k largest keys.
|
|
func NewTopK(k int) *TopK { return &TopK{k: k} }
|
|
|
|
// Offer considers one candidate.
|
|
func (t *TopK) Offer(id NodeID, key int64) {
|
|
if t.k <= 0 {
|
|
return
|
|
}
|
|
if len(t.h) < t.k {
|
|
heap.Push(&t.h, ranked{id, key})
|
|
return
|
|
}
|
|
if r := t.h[0]; key > r.key || key == r.key && id < r.id {
|
|
t.h[0] = ranked{id, key}
|
|
heap.Fix(&t.h, 0)
|
|
}
|
|
}
|
|
|
|
// Sorted returns the collected IDs, largest key first (ties by ID).
|
|
func (t *TopK) Sorted() []NodeID {
|
|
rs := append([]ranked(nil), t.h...)
|
|
sort.Slice(rs, func(i, j int) bool {
|
|
if rs[i].key != rs[j].key {
|
|
return rs[i].key > rs[j].key
|
|
}
|
|
return rs[i].id < rs[j].id
|
|
})
|
|
out := make([]NodeID, len(rs))
|
|
for i, r := range rs {
|
|
out[i] = r.id
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Walk visits id and all its descendants depth-first (iteratively).
|
|
func (t *Tree) Walk(id NodeID, fn func(NodeID, *Node) bool) {
|
|
stack := []NodeID{id}
|
|
for len(stack) > 0 {
|
|
cur := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
n := t.Node(cur)
|
|
if !fn(cur, n) {
|
|
continue
|
|
}
|
|
for c := n.FirstChild; c != NoNode; c = t.Node(c).NextSibling {
|
|
stack = append(stack, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Top returns the k nodes beneath (and including) root with the largest key,
|
|
// considering only nodes accepted by keep.
|
|
func (t *Tree) Top(root NodeID, k int, keep func(*Node) bool, key func(*Node) int64) []NodeID {
|
|
tk := NewTopK(k)
|
|
t.Walk(root, func(id NodeID, n *Node) bool {
|
|
if keep(n) {
|
|
tk.Offer(id, key(n))
|
|
}
|
|
return true
|
|
})
|
|
return tk.Sorted()
|
|
}
|
|
|
|
// SortedChildren returns the children of id ordered by size descending.
|
|
func (t *Tree) SortedChildren(id NodeID, m SizeMode) []NodeID {
|
|
var out []NodeID
|
|
t.Children(id, func(c NodeID, _ *Node) { out = append(out, c) })
|
|
sort.Slice(out, func(i, j int) bool {
|
|
a, b := t.Node(out[i]).Total(m), t.Node(out[j]).Total(m)
|
|
if a != b {
|
|
return a > b
|
|
}
|
|
return out[i] < out[j]
|
|
})
|
|
return out
|
|
}
|
|
|
|
// ExtStat summarises one extension.
|
|
type ExtStat struct {
|
|
Ext string
|
|
Cat Category
|
|
Count int64
|
|
Size int64 // logical
|
|
Alloc int64
|
|
}
|
|
|
|
// ExtStats aggregates file statistics by extension beneath root.
|
|
func (t *Tree) ExtStats(root NodeID) []ExtStat {
|
|
stats := make([]ExtStat, t.ExtCount())
|
|
t.Walk(root, func(_ NodeID, n *Node) bool {
|
|
if n.Kind == KindFile && n.Flags&FlagHardlinkDup == 0 {
|
|
s := &stats[n.Ext]
|
|
s.Count++
|
|
s.Size += n.Size
|
|
s.Alloc += n.Alloc
|
|
s.Cat = n.Cat
|
|
}
|
|
return true
|
|
})
|
|
out := stats[:0]
|
|
for i, s := range stats {
|
|
if s.Count > 0 {
|
|
s.Ext = t.exts[i]
|
|
s.Cat = CategoryFor(s.Ext, false, false)
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|