test: add image details perf regression coverage

This commit is contained in:
Harold Hunt 2026-02-22 13:07:18 -05:00
commit 0e71d0bc23
3 changed files with 173 additions and 28 deletions

View file

@ -6,11 +6,8 @@ import (
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key"
"github.com/wagoodman/dive/internal/log"
"strconv"
"strings"
"github.com/awesome-gocui/gocui"
"github.com/dustin/go-humanize"
"github.com/wagoodman/dive/dive/filetree"
)
@ -80,22 +77,11 @@ func (v *ImageDetails) Setup(body, header *gocui.View) error {
// 2. the estimated wasted image space
// 3. a list of inefficient file allocations
func (v *ImageDetails) Render() error {
analysisTemplate := "%5s %12s %-s\n"
inefficiencyReport := fmt.Sprintf(format.Header(analysisTemplate), "Count", "Total Space", "Path")
var wastedSpace int64
for idx := 0; idx < len(v.inefficiencies); idx++ {
data := v.inefficiencies[len(v.inefficiencies)-1-idx]
wastedSpace += data.CumulativeSize
inefficiencyReport += fmt.Sprintf(analysisTemplate, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
body, err := renderImageDetailsBody(v.imageName, v.imageSize, v.efficiency, v.inefficiencies)
if err != nil {
return err
}
imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), v.imageName)
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(v.imageSize))
efficiencyStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*v.efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
v.gui.Update(func(g *gocui.Gui) error {
width, _ := v.body.Size()
@ -106,18 +92,8 @@ func (v *ImageDetails) Render() error {
if err != nil {
log.WithFields("error", err).Debug("unable to write to buffer")
}
var lines = []string{
imageNameStr,
imageSizeStr,
wastedSpaceStr,
efficiencyStr,
" ", // to avoid an empty line so CursorDown can work as expected
inefficiencyReport,
}
v.body.Clear()
_, err = fmt.Fprintln(v.body, strings.Join(lines, "\n"))
_, err = fmt.Fprintln(v.body, body)
if err != nil {
log.WithFields("error", err).Debug("unable to write to buffer")
}

View file

@ -0,0 +1,48 @@
package view
import (
"fmt"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format"
"github.com/wagoodman/dive/dive/filetree"
)
const imageDetailsAnalysisTemplate = "%5s %12s %-s\n"
// NOTE: this intentionally retains the original string concatenation behavior.
// A regression test will fail until this is optimized in a follow-up commit.
func renderImageDetailsBody(imageName string, imageSize uint64, efficiency float64, inefficiencies filetree.EfficiencySlice) (string, error) {
inefficiencyReport := fmt.Sprintf(format.Header(imageDetailsAnalysisTemplate), "Count", "Total Space", "Path")
var wastedSpace int64
for idx := 0; idx < len(inefficiencies); idx++ {
data := inefficiencies[len(inefficiencies)-1-idx]
wastedSpace += data.CumulativeSize
inefficiencyReport += fmt.Sprintf(
imageDetailsAnalysisTemplate,
strconv.Itoa(len(data.Nodes)),
humanize.Bytes(uint64(data.CumulativeSize)),
data.Path,
)
}
imageNameStr := fmt.Sprintf("%s %s", format.Header("Image name:"), imageName)
imageSizeStr := fmt.Sprintf("%s %s", format.Header("Total Image size:"), humanize.Bytes(imageSize))
efficiencyStr := fmt.Sprintf("%s %d %%", format.Header("Image efficiency score:"), int(100.0*efficiency))
wastedSpaceStr := fmt.Sprintf("%s %s", format.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
var lines = []string{
imageNameStr,
imageSizeStr,
wastedSpaceStr,
efficiencyStr,
" ", // to avoid an empty line so CursorDown can work as expected
inefficiencyReport,
}
return strings.Join(lines, "\n"), nil
}

View file

@ -0,0 +1,121 @@
package view
import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"runtime"
"strconv"
"testing"
"time"
"github.com/lunixbochs/vtclean"
"github.com/stretchr/testify/require"
"github.com/wagoodman/dive/dive/filetree"
)
func imageDetailsContentSourcePath(t *testing.T) string {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
require.True(t, ok, "unable to determine test file path")
return filepath.Join(filepath.Dir(thisFile), "image_details_content.go")
}
func imageDetailsContentAST(t *testing.T) *ast.File {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, imageDetailsContentSourcePath(t), nil, parser.ParseComments)
require.NoError(t, err, "unable to parse image_details_content.go")
return file
}
func funcDeclByName(file *ast.File, funcName string) *ast.FuncDecl {
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Name == nil || fn.Name.Name != funcName || fn.Recv != nil {
continue
}
return fn
}
return nil
}
// Regression guard: report construction must not use repeated string appends
// (`+=`) in loops, which can reintroduce O(n^2) behavior.
func TestRenderImageDetailsBodyAvoidsInLoopStringAddAssign(t *testing.T) {
file := imageDetailsContentAST(t)
fn := funcDeclByName(file, "renderImageDetailsBody")
require.NotNil(t, fn, "missing renderImageDetailsBody declaration")
found := false
ast.Inspect(fn.Body, func(node ast.Node) bool {
assign, ok := node.(*ast.AssignStmt)
if !ok || assign.Tok != token.ADD_ASSIGN || len(assign.Lhs) != 1 {
return true
}
lhs, ok := assign.Lhs[0].(*ast.Ident)
if ok && lhs.Name == "inefficiencyReport" {
found = true
return false
}
return true
})
require.False(t, found, "renderImageDetailsBody reintroduced inefficiencyReport += ... in-loop concatenation")
}
func TestRenderImageDetailsBodyIncludesExpectedContent(t *testing.T) {
body, err := renderImageDetailsBody(
"ghcr.io/openclaw/openclaw:test",
10*1024*1024,
0.72,
filetree.EfficiencySlice{
{Path: "/a", Nodes: []*filetree.FileNode{{}, {}, {}}, CumulativeSize: 40},
{Path: "/b", Nodes: []*filetree.FileNode{{}}, CumulativeSize: 60},
},
)
require.NoError(t, err)
clean := vtclean.Clean(body, false)
require.Contains(t, clean, "Image name:")
require.Contains(t, clean, "ghcr.io/openclaw/openclaw:test")
require.Contains(t, clean, "Total Image size:")
require.Contains(t, clean, "Potential wasted space:")
require.Contains(t, clean, "Image efficiency score:")
require.Contains(t, clean, "/a")
require.Contains(t, clean, "/b")
}
// Integration-style perf check: rendering should scale near-linearly as
// inefficiency row counts grow.
func TestRenderImageDetailsBodyScaling(t *testing.T) {
if testing.Short() {
t.Skip("skipping perf-sensitive scaling check in short mode")
}
buildDuration := func(entries, rounds int) time.Duration {
payload := make(filetree.EfficiencySlice, 0, entries)
for i := 0; i < entries; i++ {
payload = append(payload, &filetree.EfficiencyData{
Path: "/path/" + strconv.Itoa(i),
Nodes: make([]*filetree.FileNode, (i%3)+1),
CumulativeSize: int64(64 + i%1024),
})
}
start := time.Now()
for i := 0; i < rounds; i++ {
_, err := renderImageDetailsBody("img:test", 1234, 0.9, payload)
require.NoError(t, err)
}
return time.Since(start)
}
small := buildDuration(1200, 10)
large := buildDuration(2400, 10)
ratio := float64(large) / float64(small)
require.Less(t, ratio, 3.2, "expected near-linear scaling, got ratio %.2f (small=%s large=%s)", ratio, small, large)
}