Files
mapsize/internal/snapshot/compare.go
T

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
}