Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shallow clone trees to save on memory usage #268

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions pkg/filetree/filetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ func New() *FileTree {
}
}

// Copy returns a Copy of the current FileTree.
// Clone returns a shallow Copy of the current FileTree.
func (t *FileTree) Clone() (ReadWriter, error) {
ct := New()
ct.tree = t.tree.Clone()
return ct, nil
}

// Copy returns a deep Copy of the current FileTree.
func (t *FileTree) Copy() (ReadWriter, error) {
ct := New()
ct.tree = t.tree.Copy()
Expand Down Expand Up @@ -781,7 +788,8 @@ func (t *FileTree) Walk(fn func(path file.Path, f filenode.FileNode) error, cond

// Merge takes the given Tree and combines it with the current Tree, preferring files in the other Tree if there
// are path conflicts. This is the basis function for squashing (where the current Tree is the bottom Tree and the
// given Tree is the top Tree).
// given Tree is the top Tree). Note: existing nodes are not mutated during this operation, they are replaced with
// a node from the other tree.
//
//nolint:gocognit,funlen
func (t *FileTree) Merge(upper Reader) error {
Expand Down
1 change: 1 addition & 0 deletions pkg/filetree/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type PathReader interface {

type Copier interface {
Copy() (ReadWriter, error)
Clone() (ReadWriter, error)
}

type Walker interface {
Expand Down
8 changes: 6 additions & 2 deletions pkg/filetree/union_filetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ func (u *UnionFileTree) Squash() (ReadWriter, error) {
case 0:
return New(), nil
case 1:
return u.trees[0].Copy()
// important: use clone over copy to reduce memory footprint. If callers need a distinct tree they can call
// copy after the fact.
return u.trees[0].Clone()
}

var squashedTree ReadWriter
var err error
for layerIdx, refTree := range u.trees {
if layerIdx == 0 {
squashedTree, err = refTree.Copy()
// important: use clone over copy to reduce memory footprint. If callers need a distinct tree they can call
// copy after the fact.
squashedTree, err = refTree.Clone()
if err != nil {
return nil, err
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tree

import (
"fmt"
"maps"

"github.com/anchore/stereoscope/pkg/tree/node"
)
Expand All @@ -22,6 +23,16 @@ func NewTree() *Tree {
}
}

// Clone returns a shallow copy of the Tree.
func (t *Tree) Clone() *Tree {
ct := NewTree()
ct.nodes = maps.Clone(t.nodes)
ct.parent = maps.Clone(t.parent)
ct.children = maps.Clone(t.children)
return ct
}

// Copy returns a deep copy of the Tree.
func (t *Tree) Copy() *Tree {
ct := NewTree()
for k, v := range t.nodes {
Expand Down
62 changes: 62 additions & 0 deletions test/integration/path_type_change_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package integration

import (
"testing"

"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree/filenode"
"github.com/anchore/stereoscope/pkg/imagetest"
)

func TestPathTypeChangeImage(t *testing.T) {
image := imagetest.GetFixtureImage(t, "docker-archive", "image-path-type-change")

layerAssertions := []map[string]file.Type{
make(map[string]file.Type),
{
"/chimera/a.txt": file.TypeRegular,
"/chimera/b.txt": file.TypeRegular,
"/chimera": file.TypeDirectory,
},
{
"/chimera": file.TypeDirectory,
},
make(map[string]file.Type),
{
"/chimera": file.TypeRegular,
},
make(map[string]file.Type),
{
"/chimera": file.TypeSymLink,
},
make(map[string]file.Type),
{
"/chimera": file.TypeDirectory,
},
}

for idx, layer := range image.Layers {
assertions := layerAssertions[idx]
err := layer.SquashedTree.Walk(func(path file.Path, f filenode.FileNode) error {
expect, ok := assertions[string(path)]
if !ok {
return nil
}
if f.FileType != expect {
t.Errorf("at %v got %v want %v", path, f.FileType, expect)
}
delete(assertions, string(path))
return nil
}, nil)
if err != nil {
t.Error(err)
}
}
for i, a := range layerAssertions {
if len(a) > 0 {
for k, v := range a {
t.Errorf("for layer %v, never saw %v of type %v", i, k, v)
}
}
}
}
20 changes: 20 additions & 0 deletions test/integration/test-fixtures/image-path-type-change/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM busybox:1.37.0@sha256:db142d433cdde11f10ae479dbf92f3b13d693fd1c91053da9979728cceb1dc68

# makes /chimera/a.txt /chimera/b.txt
COPY . /

# make a layer where the dir is empty
RUN rm /chimera/a.txt /chimera/b.txt

RUN rmdir /chimera

RUN touch /chimera

RUN rm /chimera

RUN ln -s /etc/hostname /chimera

RUN unlink /chimera

RUN mkdir /chimera

Empty file.
Empty file.
Loading