internal/core/adt: fix benign memory leak
This fixes a memory leak that would cause a nodeContext
to be not recycled. As the nodeContext would have been
garbage collected anyway, this was just a matter of
performance.
A leak checker has been added to the tests. There are
some seemingly benign leaks remaining, so the test
uses Skipf for now.
Change-Id: I172a5a9f76e30a69cca398bd14f243ee13d96765
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8304
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/core/adt/disjunct.go b/internal/core/adt/disjunct.go
index 7f3475f..e7db3cd 100644
--- a/internal/core/adt/disjunct.go
+++ b/internal/core/adt/disjunct.go
@@ -330,6 +330,7 @@
// time that a clone is needed and must be nil. Conjuncts no longer needed and
// can become nil. All other fields can be copied shallowly.
func clone(v Vertex) Vertex {
+ v.state = nil
if a := v.Arcs; len(a) > 0 {
v.Arcs = make([]*Vertex, len(a))
for i, arc := range a {
diff --git a/internal/core/adt/eval.go b/internal/core/adt/eval.go
index b00b0b0..f873cf1 100644
--- a/internal/core/adt/eval.go
+++ b/internal/core/adt/eval.go
@@ -57,7 +57,18 @@
Allocs int
}
+// Leaks reports the number of nodeContext structs leaked. These are typically
+// benign, as they will just be garbage collected, as long as the pointer from
+// the original nodes has been eliminated or the original nodes are also not
+// referred to. But Leaks may have notable impact on performance, and thus
+// should be avoided.
+func (s *Stats) Leaks() int {
+ return s.Allocs + s.Reused - s.Freed
+}
+
var stats = template.Must(template.New("stats").Parse(`{{"" -}}
+
+Leaks: {{.Leaks}}
Freed: {{.Freed}}
Reused: {{.Reused}}
Allocs: {{.Allocs}}
@@ -330,10 +341,6 @@
}
n.expandDisjuncts(disState, n, maybeDefault, false)
- // If the state has changed, it is because a disjunct has been run. In this case, our node will have completed, and it will
- // set a value soon.
- v.state = n // alternatively, set to nil
-
for _, d := range n.disjuncts {
d.free()
}
@@ -373,6 +380,11 @@
// TODO: how to represent closedness information? Do we need it?
}
+ // If the state has changed, it is because a disjunct has been run, or
+ // because a single disjunct has replaced it. Restore the old state as
+ // to not confuse memory management.
+ v.state = n
+
if state != Finalized {
return
}
diff --git a/internal/core/adt/eval_test.go b/internal/core/adt/eval_test.go
index 017a021..2f422be 100644
--- a/internal/core/adt/eval_test.go
+++ b/internal/core/adt/eval_test.go
@@ -64,7 +64,11 @@
ctx := e.NewContext(v)
v.Finalize(ctx)
- t.Log(e.Stats())
+ stats := ctx.Stats()
+ t.Log(stats)
+ if n := stats.Leaks(); n > 0 {
+ t.Skipf("%d leaks reported", n)
+ }
if b := validate.Validate(ctx, v, &validate.Config{
AllErrors: true,