Files
mapsize/internal/inventory/query.go
T
Andy Dixon 61fd9bf711 Add filesystem inventory model
Append-only chunked node arena with stable NodeIDs, intrusive child
lists, per-directory category totals and batched ancestor propagation.
2026-10-02 08:02:32 +01:00

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
}