178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package snapshot
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/andydixon/mapsize/internal/inventory"
|
|
)
|
|
|
|
// Diff relates two inventories by path. Both trees must not be mutated
|
|
// while a Diff is in use (snapshots and finished scans satisfy this).
|
|
type Diff struct {
|
|
Old, New *inventory.Tree
|
|
// OldOf maps a New NodeID to the matching Old NodeID, or NoNode if the
|
|
// entry is new.
|
|
OldOf []inventory.NodeID
|
|
// Removed lists the topmost Old nodes with no counterpart in New.
|
|
Removed []inventory.NodeID
|
|
// RemovedUnder sums, per New directory, the sizes of removed children
|
|
// (in both size modes) so growth colouring can account for them.
|
|
removedUnder map[inventory.NodeID][2]int64
|
|
}
|
|
|
|
// Compare matches nodes by name, directory by directory, from the roots.
|
|
func Compare(old, new *inventory.Tree) *Diff {
|
|
d := &Diff{Old: old, New: new, OldOf: make([]inventory.NodeID, new.Len()),
|
|
removedUnder: map[inventory.NodeID][2]int64{}}
|
|
for i := range d.OldOf {
|
|
d.OldOf[i] = inventory.NoNode
|
|
}
|
|
type pair struct{ o, n inventory.NodeID }
|
|
stack := []pair{{old.Root(), new.Root()}}
|
|
for len(stack) > 0 {
|
|
p := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
d.OldOf[p.n] = p.o
|
|
on, nn := old.Node(p.o), new.Node(p.n)
|
|
if !on.IsDir() || !nn.IsDir() {
|
|
continue
|
|
}
|
|
byName := map[string]inventory.NodeID{}
|
|
old.Children(p.o, func(id inventory.NodeID, n *inventory.Node) { byName[n.Name] = id })
|
|
new.Children(p.n, func(id inventory.NodeID, n *inventory.Node) {
|
|
if oid, ok := byName[n.Name]; ok {
|
|
delete(byName, n.Name)
|
|
stack = append(stack, pair{oid, id})
|
|
}
|
|
})
|
|
for _, oid := range byName {
|
|
d.Removed = append(d.Removed, oid)
|
|
o := old.Node(oid)
|
|
r := d.removedUnder[p.n]
|
|
r[0] += o.TotAlloc
|
|
r[1] += o.TotSize
|
|
d.removedUnder[p.n] = r
|
|
}
|
|
}
|
|
sort.Slice(d.Removed, func(i, j int) bool { return d.Removed[i] < d.Removed[j] })
|
|
return d
|
|
}
|
|
|
|
// OldSize returns the matching old size of a New node and whether it existed.
|
|
func (d *Diff) OldSize(id inventory.NodeID, m inventory.SizeMode) (int64, bool) {
|
|
o := d.OldOf[id]
|
|
if o == inventory.NoNode {
|
|
return 0, false
|
|
}
|
|
return d.Old.Node(o).Total(m), true
|
|
}
|
|
|
|
// Delta returns new minus old size for a New node (full size if new).
|
|
func (d *Diff) Delta(id inventory.NodeID, m inventory.SizeMode) int64 {
|
|
old, _ := d.OldSize(id, m)
|
|
return d.New.Node(id).Total(m) - old
|
|
}
|
|
|
|
// Status classifies a node.
|
|
type Status uint8
|
|
|
|
const (
|
|
Unchanged Status = iota
|
|
Added
|
|
Removed
|
|
Grew
|
|
Shrank
|
|
)
|
|
|
|
func (s Status) String() string {
|
|
return [...]string{"unchanged", "new", "removed", "increased", "decreased"}[s]
|
|
}
|
|
|
|
// StatusOf classifies a New node.
|
|
func (d *Diff) StatusOf(id inventory.NodeID, m inventory.SizeMode) Status {
|
|
if d.OldOf[id] == inventory.NoNode {
|
|
return Added
|
|
}
|
|
switch delta := d.Delta(id, m); {
|
|
case delta > 0:
|
|
return Grew
|
|
case delta < 0:
|
|
return Shrank
|
|
}
|
|
return Unchanged
|
|
}
|
|
|
|
// Change is one row in a change report.
|
|
type Change struct {
|
|
Path string
|
|
Status Status
|
|
Delta int64
|
|
Old int64
|
|
New int64
|
|
Node inventory.NodeID // in New (NoNode for removed)
|
|
}
|
|
|
|
// Changes returns the k most significant changes. A directory is listed
|
|
// only if its change is not almost entirely explained by a single child, so
|
|
// the report points at /var/lib/postgresql rather than /, /var and /var/lib.
|
|
func (d *Diff) Changes(k int, m inventory.SizeMode) []Change {
|
|
var out []Change
|
|
for i := range d.New.Len() {
|
|
id := inventory.NodeID(i)
|
|
delta := d.Delta(id, m)
|
|
if delta == 0 {
|
|
continue
|
|
}
|
|
n := d.New.Node(id)
|
|
if d.OldOf[id] != inventory.NoNode && n.IsDir() && explainedByChild(d, id, delta, m) {
|
|
continue
|
|
}
|
|
if p := n.Parent; p != inventory.NoNode && d.OldOf[p] == inventory.NoNode {
|
|
continue // inside a wholly new directory; the directory is listed
|
|
}
|
|
old, _ := d.OldSize(id, m)
|
|
out = append(out, Change{Path: d.New.Path(id), Status: d.StatusOf(id, m), Delta: delta, Old: old, New: n.Total(m), Node: id})
|
|
}
|
|
for _, oid := range d.Removed {
|
|
o := d.Old.Node(oid)
|
|
out = append(out, Change{Path: d.Old.Path(oid), Status: Removed, Delta: -o.Total(m), Old: o.Total(m), Node: inventory.NoNode})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
a, b := abs(out[i].Delta), abs(out[j].Delta)
|
|
if a != b {
|
|
return a > b
|
|
}
|
|
return out[i].Path < out[j].Path
|
|
})
|
|
if k > 0 && len(out) > k {
|
|
out = out[:k]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func explainedByChild(d *Diff, id inventory.NodeID, delta int64, m inventory.SizeMode) bool {
|
|
explained := false
|
|
d.New.Children(id, func(c inventory.NodeID, _ *inventory.Node) {
|
|
if cd := d.Delta(c, m); (cd > 0) == (delta > 0) && abs(cd)*10 >= abs(delta)*8 {
|
|
explained = true
|
|
}
|
|
})
|
|
if r, ok := d.removedUnder[id]; ok && !explained {
|
|
v := r[0]
|
|
if m == inventory.SizeLogical {
|
|
v = r[1]
|
|
}
|
|
if delta < 0 && v*10 >= abs(delta)*8 {
|
|
explained = true
|
|
}
|
|
}
|
|
return explained
|
|
}
|
|
|
|
func abs(v int64) int64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|