mirror of
https://github.com/wagoodman/dive
synced 2026-03-14 22:35:50 +01:00
test: add image details perf regression coverage
This commit is contained in:
parent
d6c691947f
commit
0e71d0bc23
3 changed files with 173 additions and 28 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
48
cmd/dive/cli/internal/ui/v1/view/image_details_content.go
Normal file
48
cmd/dive/cli/internal/ui/v1/view/image_details_content.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue