hs-test: memory leak testing
add infra for memory leak testing
Type: test
Change-Id: I882e8dbb360597cdb82ad52682725f7d39b2df24
Signed-off-by: Matus Fabian <matfabia@cisco.com>
diff --git a/extras/hs-test/Makefile b/extras/hs-test/Makefile
index 033d16b..83a42c8 100644
--- a/extras/hs-test/Makefile
+++ b/extras/hs-test/Makefile
@@ -62,6 +62,7 @@
@echo "Make targets:"
@echo " test - run tests"
@echo " test-debug - run tests (vpp debug image)"
+ @echo " test-leak - run memory leak tests (vpp debug image)"
@echo " build - build test infra"
@echo " build-cov - coverage build of VPP and Docker images"
@echo " build-debug - build test infra (vpp debug image)"
@@ -143,6 +144,10 @@
@$(MAKE) -C ../.. test-cov-post HS_TEST=1
@bash ./script/compress.sh
+.PHONY: test-leak
+test-leak: .deps.ok .build_debug.ok
+ @bash ./hs_test.sh --test=$(TEST) --debug_build=true --leak_check=true --vppsrc=$(VPPSRC)
+
.PHONY: build-go
build-go:
go build ./tools/http_server
diff --git a/extras/hs-test/README.rst b/extras/hs-test/README.rst
index 7841211..8a49ac6 100644
--- a/extras/hs-test/README.rst
+++ b/extras/hs-test/README.rst
@@ -307,8 +307,52 @@
If a test consists of more VPP instances then this is done for each of them.
+**Memory leak testing**
-**Eternal dependencies**
+It is possible to use VPP memory traces to diagnose if and where memory leaks happen by comparing of two traces at different point in time.
+You can do it by test like following:
+
+::
+
+ func MemLeakTest(s *NoTopoSuite) {
+ s.SkipUnlessLeakCheck() // test is excluded from usual test run
+ vpp := s.GetContainerByName("vpp").VppInstance
+ /* do your configuration here */
+ vpp.Disconnect() // no goVPP less noise
+ vpp.EnableMemoryTrace() // enable memory traces
+ traces1, err := vpp.GetMemoryTrace() // get first sample
+ s.AssertNil(err, fmt.Sprint(err))
+ vpp.Vppctl("test mem-leak") // execute some action
+ traces2, err := vpp.GetMemoryTrace() // get second sample
+ s.AssertNil(err, fmt.Sprint(err))
+ vpp.MemLeakCheck(traces1, traces2) // compare samples and generate report
+ }
+
+To get your memory leak report run following command:
+
+::
+
+ $ make test-leak TEST=MemLeakTest
+ ...
+ NoTopoSuiteSolo mem_leak_test.go/MemLeakTest [SOLO]
+ /home/matus/vpp/extras/hs-test/infra/suite_no_topo.go:113
+
+ Report Entries >>
+
+ SUMMARY: 112 byte(s) leaked in 1 allocation(s)
+ - /home/matus/vpp/extras/hs-test/infra/vppinstance.go:624 @ 07/19/24 15:53:33.539
+
+ leak of 112 byte(s) in 1 allocation(s) from:
+ #0 clib_mem_heap_alloc_aligned + 0x31
+ #1 _vec_alloc_internal + 0x113
+ #2 _vec_validate + 0x81
+ #3 leak_memory_fn + 0x4f
+ #4 0x7fc167815ac3
+ #5 0x7fc1678a7850
+ << Report Entries
+ ------------------------------
+
+**External dependencies**
* Linux tools ``ip``, ``brctl``
* Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made,
diff --git a/extras/hs-test/hs_test.sh b/extras/hs-test/hs_test.sh
index 803b8f7..acad7eb 100644
--- a/extras/hs-test/hs_test.sh
+++ b/extras/hs-test/hs_test.sh
@@ -7,8 +7,10 @@
persist_set=0
unconfigure_set=0
debug_set=0
+leak_check_set=0
debug_build=
ginkgo_args=
+tc_name=
for i in "$@"
do
@@ -74,6 +76,13 @@
args="$args -cpu0"
fi
;;
+ --leak_check=*)
+ leak_check="${i#*=}"
+ if [ "$leak_check" = "true" ]; then
+ args="$args -leak_check"
+ leak_check_set=1
+ fi
+ ;;
esac
done
@@ -97,6 +106,16 @@
exit 1
fi
+if [ $leak_check_set -eq 1 ]; then
+ if [ $single_test -eq 0 ]; then
+ echo "a single test has to be specified when leak_check is set"
+ exit 1
+ fi
+ ginkgo_args="--focus $tc_name"
+ sudo -E go run github.com/onsi/ginkgo/v2/ginkgo $ginkgo_args -- $args
+ exit 0
+fi
+
mkdir -p summary
# shellcheck disable=SC2086
sudo -E go run github.com/onsi/ginkgo/v2/ginkgo --no-color --trace --json-report=summary/report.json $ginkgo_args -- $args
diff --git a/extras/hs-test/infra/container.go b/extras/hs-test/infra/container.go
index 5093398..44f141a 100644
--- a/extras/hs-test/infra/container.go
+++ b/extras/hs-test/infra/container.go
@@ -4,6 +4,7 @@
"bytes"
"context"
"fmt"
+ "github.com/docker/go-units"
"os"
"os/exec"
"slices"
@@ -15,7 +16,6 @@
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stdcopy"
- "github.com/docker/go-units"
"github.com/edwarnicke/exechelper"
. "github.com/onsi/ginkgo/v2"
)
@@ -382,6 +382,11 @@
return nil
}
+func (c *Container) GetFile(sourceFileName, targetFileName string) error {
+ cmd := exec.Command("docker", "cp", c.Name+":"+sourceFileName, targetFileName)
+ return cmd.Run()
+}
+
/*
* Executes in detached mode so that the started application can continue to run
* without blocking execution of test
diff --git a/extras/hs-test/infra/hst_suite.go b/extras/hs-test/infra/hst_suite.go
index 975e01d..2cf241a 100644
--- a/extras/hs-test/infra/hst_suite.go
+++ b/extras/hs-test/infra/hst_suite.go
@@ -35,6 +35,7 @@
var VppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
var IsDebugBuild = flag.Bool("debug_build", false, "some paths are different with debug build")
var UseCpu0 = flag.Bool("cpu0", false, "use cpu0")
+var IsLeakCheck = flag.Bool("leak_check", false, "run leak-check tests")
var NumaAwareCpuAlloc bool
var SuiteTimeout time.Duration
@@ -285,6 +286,11 @@
}
}
+func (s *HstSuite) SkipUnlessLeakCheck() {
+ if !*IsLeakCheck {
+ s.Skip("leak-check tests excluded")
+ }
+}
func (s *HstSuite) ResetContainers() {
for _, container := range s.StartedContainers {
container.stop()
diff --git a/extras/hs-test/infra/vppinstance.go b/extras/hs-test/infra/vppinstance.go
index d4f5700..dfb236b 100644
--- a/extras/hs-test/infra/vppinstance.go
+++ b/extras/hs-test/infra/vppinstance.go
@@ -2,6 +2,7 @@
import (
"context"
+ "encoding/json"
"fmt"
"go.fd.io/govpp/binapi/ethernet_types"
"io"
@@ -97,6 +98,13 @@
SkipCores int
}
+type VppMemTrace struct {
+ Count int `json:"count"`
+ Size int `json:"bytes"`
+ Sample string `json:"sample"`
+ Traceback []string `json:"traceback"`
+}
+
func (vpp *VppInstance) getSuite() *HstSuite {
return vpp.Container.Suite
}
@@ -535,3 +543,83 @@
return c.Close().ToString()
}
+
+// EnableMemoryTrace enables memory traces of VPP main-heap
+func (vpp *VppInstance) EnableMemoryTrace() {
+ vpp.getSuite().Log(vpp.Vppctl("memory-trace on main-heap"))
+}
+
+// GetMemoryTrace dumps memory traces for analysis
+func (vpp *VppInstance) GetMemoryTrace() ([]VppMemTrace, error) {
+ var trace []VppMemTrace
+ vpp.getSuite().Log(vpp.Vppctl("save memory-trace trace.json"))
+ err := vpp.Container.GetFile("/tmp/trace.json", "/tmp/trace.json")
+ if err != nil {
+ return nil, err
+ }
+ fileBytes, err := os.ReadFile("/tmp/trace.json")
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(fileBytes, &trace)
+ if err != nil {
+ return nil, err
+ }
+ return trace, nil
+}
+
+// memTracesSuppressCli filter out CLI related samples
+func memTracesSuppressCli(traces []VppMemTrace) []VppMemTrace {
+ var filtered []VppMemTrace
+ for i := 0; i < len(traces); i++ {
+ isCli := false
+ for j := 0; j < len(traces[i].Traceback); j++ {
+ if strings.Contains(traces[i].Traceback[j], "unix_cli") {
+ isCli = true
+ break
+ }
+ }
+ if !isCli {
+ filtered = append(filtered, traces[i])
+ }
+ }
+ return filtered
+}
+
+// MemLeakCheck compares memory traces at different point in time, analyzes if memory leaks happen and produces report
+func (vpp *VppInstance) MemLeakCheck(first, second []VppMemTrace) {
+ totalBytes := 0
+ totalCounts := 0
+ trace1 := memTracesSuppressCli(first)
+ trace2 := memTracesSuppressCli(second)
+ report := ""
+ for i := 0; i < len(trace2); i++ {
+ match := false
+ for j := 0; j < len(trace1); j++ {
+ if trace1[j].Sample == trace2[i].Sample {
+ if trace2[i].Size > trace1[j].Size {
+ deltaBytes := trace2[i].Size - trace1[j].Size
+ deltaCounts := trace2[i].Count - trace1[j].Count
+ report += fmt.Sprintf("grow %d byte(s) in %d allocation(s) from:\n", deltaBytes, deltaCounts)
+ for j := 0; j < len(trace2[i].Traceback); j++ {
+ report += fmt.Sprintf("\t#%d %s\n", j, trace2[i].Traceback[j])
+ }
+ totalBytes += deltaBytes
+ totalCounts += deltaCounts
+ }
+ match = true
+ break
+ }
+ }
+ if !match {
+ report += fmt.Sprintf("\nleak of %d byte(s) in %d allocation(s) from:\n", trace2[i].Size, trace2[i].Count)
+ for j := 0; j < len(trace2[i].Traceback); j++ {
+ report += fmt.Sprintf("\t#%d %s\n", j, trace2[i].Traceback[j])
+ }
+ totalBytes += trace2[i].Size
+ totalCounts += trace2[i].Count
+ }
+ }
+ summary := fmt.Sprintf("\nSUMMARY: %d byte(s) leaked in %d allocation(s)\n", totalBytes, totalCounts)
+ AddReportEntry(summary, report)
+}
diff --git a/extras/hs-test/mem_leak_test.go b/extras/hs-test/mem_leak_test.go
new file mode 100644
index 0000000..76966ae
--- /dev/null
+++ b/extras/hs-test/mem_leak_test.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ . "fd.io/hs-test/infra"
+ "fmt"
+)
+
+func init() {
+ RegisterNoTopoSoloTests(MemLeakTest)
+}
+
+func MemLeakTest(s *NoTopoSuite) {
+ s.SkipUnlessLeakCheck()
+ vpp := s.GetContainerByName("vpp").VppInstance
+ /* no goVPP less noise */
+ vpp.Disconnect()
+ vpp.EnableMemoryTrace()
+ traces1, err := vpp.GetMemoryTrace()
+ s.AssertNil(err, fmt.Sprint(err))
+ vpp.Vppctl("test mem-leak")
+ traces2, err := vpp.GetMemoryTrace()
+ s.AssertNil(err, fmt.Sprint(err))
+ vpp.MemLeakCheck(traces1, traces2)
+}