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

feat: delete directory items in parallel #340

Merged
merged 3 commits into from
Apr 14, 2024
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ style:
background-color: "#ff0000"
```

## Deletion in background and in parallel (experimental)

Gdu can delete items in the background, thus not blocking the UI for additional work.
To enable:

```
echo "delete-in-background: true" >> ~/.gdu.yaml
```

Directory items can be also deleted in parallel, which can increase the speed of deletion.
To enable:

```
echo "delete-in-parallel: true" >> ~/.gdu.yaml
```

## Memory usage

### Automatic balancing
Expand Down
6 changes: 6 additions & 0 deletions cmd/gdu/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
WriteConfig bool `yaml:"-"`
ChangeCwd bool `yaml:"change-cwd"`
DeleteInBackground bool `yaml:"delete-in-background"`
DeleteInParallel bool `yaml:"delete-in-parallel"`
Style Style `yaml:"style"`
Sorting Sorting `yaml:"sorting"`
}
Expand Down Expand Up @@ -286,6 +287,11 @@
ui.SetDeleteInBackground()
})
}
if a.Flags.DeleteInParallel {
opts = append(opts, func(ui *tui.UI) {
ui.SetDeleteInParallel()
})

Check warning on line 293 in cmd/gdu/app/app.go

View check run for this annotation

Codecov / codecov/patch

cmd/gdu/app/app.go#L291-L293

Added lines #L291 - L293 were not covered by tests
}

ui = tui.CreateUI(
a.TermApp,
Expand Down
3 changes: 2 additions & 1 deletion internal/testanalyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/dundee/gdu/v5/internal/common"
"github.com/dundee/gdu/v5/pkg/analyze"
"github.com/dundee/gdu/v5/pkg/fs"
"github.com/dundee/gdu/v5/pkg/remove"
)

// MockedAnalyzer returns dir with files with different size exponents
Expand Down Expand Up @@ -91,7 +92,7 @@ func RemoveItemFromDirWithErr(dir fs.Item, file fs.Item) error {
// RemoveItemFromDirWithSleep returns error
func RemoveItemFromDirWithSleep(dir fs.Item, file fs.Item) error {
time.Sleep(time.Millisecond * 600)
return analyze.RemoveItemFromDir(dir, file)
return remove.RemoveItemFromDir(dir, file)
}

// RemoveItemFromDirWithSleepAndErr returns error
Expand Down
44 changes: 1 addition & 43 deletions pkg/analyze/file.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package analyze

import (
"os"
"path/filepath"
"sync"
"time"
Expand Down Expand Up @@ -231,7 +230,7 @@ func (f *Dir) UpdateStats(linkedItems fs.HardLinkedItems) {
f.Usage = totalUsage
}

// RemoveFile panics on file
// RemoveFile removes item from dir, updates size and item count
func (f *Dir) RemoveFile(item fs.Item) {
f.m.Lock()
defer f.m.Unlock()
Expand All @@ -256,44 +255,3 @@ func (f *Dir) RLock() func() {
f.m.RLock()
return f.m.RUnlock
}

// RemoveItemFromDir removes item from dir
func RemoveItemFromDir(dir fs.Item, item fs.Item) error {
err := os.RemoveAll(item.GetPath())
if err != nil {
return err
}

dir.RemoveFile(item)
return nil
}

// EmptyFileFromDir empty file from dir
func EmptyFileFromDir(dir fs.Item, file fs.Item) error {
err := os.Truncate(file.GetPath(), 0)
if err != nil {
return err
}

cur := dir.(*Dir)
for {
cur.Size -= file.GetSize()
cur.Usage -= file.GetUsage()

if cur.Parent == nil {
break
}
cur = cur.Parent.(*Dir)
}

dir.SetFiles(dir.GetFiles().Remove(file))
newFile := &File{
Name: file.GetName(),
Flag: file.GetFlag(),
Size: 0,
Parent: dir,
}
dir.AddFile(newFile)

return nil
}
120 changes: 0 additions & 120 deletions pkg/analyze/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"testing"
"time"

"github.com/dundee/gdu/v5/internal/testdir"
"github.com/dundee/gdu/v5/pkg/fs"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -201,125 +200,6 @@ func TestRemoveByNameNotInDir(t *testing.T) {
assert.Equal(t, 1, len(dir.Files))
}

func TestRemoveFile(t *testing.T) {
dir := &Dir{
File: &File{
Name: "xxx",
Size: 5,
Usage: 12,
},
ItemCount: 3,
BasePath: ".",
}

subdir := &Dir{
File: &File{
Name: "yyy",
Size: 4,
Usage: 8,
Parent: dir,
},
ItemCount: 2,
}
file := &File{
Name: "zzz",
Size: 3,
Usage: 4,
Parent: subdir,
}
dir.Files = fs.Files{subdir}
subdir.Files = fs.Files{file}

err := RemoveItemFromDir(subdir, file)
assert.Nil(t, err)

assert.Equal(t, 0, len(subdir.Files))
assert.Equal(t, 1, subdir.ItemCount)
assert.Equal(t, int64(1), subdir.Size)
assert.Equal(t, int64(4), subdir.Usage)
assert.Equal(t, 1, len(dir.Files))
assert.Equal(t, 2, dir.ItemCount)
assert.Equal(t, int64(2), dir.Size)
}

func TestTruncateFile(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

dir := &Dir{
File: &File{
Name: "test_dir",
Size: 5,
Usage: 12,
},
ItemCount: 3,
BasePath: ".",
}

subdir := &Dir{
File: &File{
Name: "nested",
Size: 4,
Usage: 8,
Parent: dir,
},
ItemCount: 2,
}
file := &File{
Name: "file2",
Size: 3,
Usage: 4,
Parent: subdir,
}
dir.Files = fs.Files{subdir}
subdir.Files = fs.Files{file}

err := EmptyFileFromDir(subdir, file)

assert.Nil(t, err)
assert.Equal(t, 1, len(subdir.Files))
assert.Equal(t, 2, subdir.ItemCount)
assert.Equal(t, int64(1), subdir.Size)
assert.Equal(t, int64(4), subdir.Usage)
assert.Equal(t, 1, len(dir.Files))
assert.Equal(t, 3, dir.ItemCount)
assert.Equal(t, int64(2), dir.Size)
}

func TestTruncateFileWithErr(t *testing.T) {
dir := &Dir{
File: &File{
Name: "xxx",
Size: 5,
Usage: 12,
},
ItemCount: 3,
BasePath: ".",
}

subdir := &Dir{
File: &File{
Name: "yyy",
Size: 4,
Usage: 8,
Parent: dir,
},
ItemCount: 2,
}
file := &File{
Name: "zzz",
Size: 3,
Usage: 4,
Parent: subdir,
}
dir.Files = fs.Files{subdir}
subdir.Files = fs.Files{file}

err := EmptyFileFromDir(subdir, file)

assert.Contains(t, err.Error(), "no such file or directory")
}

func TestUpdateStats(t *testing.T) {
dir := Dir{
File: &File{
Expand Down
3 changes: 2 additions & 1 deletion pkg/analyze/stored.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ func (f *StoredDir) SetFiles(files fs.Files) {
f.Files = files
}

// RemoveFile panics on file
// RemoveFile removes file from stored directory
// It also updates size and item count of parent directories
func (f *StoredDir) RemoveFile(item fs.Item) {
if !DefaultStorage.IsOpen() {
f.dbLock.Lock()
Expand Down
58 changes: 58 additions & 0 deletions pkg/remove/parallel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package remove

import (
"os"
"runtime"
"sync"

"github.com/dundee/gdu/v5/pkg/fs"
)

var concurrencyLimit = make(chan struct{}, 3*runtime.GOMAXPROCS(0))

// RemoveItemFromDirParallel removes item from dir
func RemoveItemFromDirParallel(dir fs.Item, item fs.Item) error {
if !item.IsDir() {
return RemoveItemFromDir(dir, item)
}
errChan := make(chan error, 1) // we show only first error
var wait sync.WaitGroup

// remove all files in the directory in parallel
for _, file := range item.GetFilesLocked() {
wait.Add(1)
go func(itemPath string) {
concurrencyLimit <- struct{}{}
defer func() { <-concurrencyLimit }()

err := os.RemoveAll(itemPath)
if err != nil {
select {
// write error to channel if it's empty
case errChan <- err:
default:

Check warning on line 33 in pkg/remove/parallel.go

View check run for this annotation

Codecov / codecov/patch

pkg/remove/parallel.go#L33

Added line #L33 was not covered by tests
}
}
wait.Done()
}(file.GetPath())
}

wait.Wait()

// check if there was an error
select {
case err := <-errChan:
return err
default:
}

// remove the directory itself
err := os.RemoveAll(item.GetPath())
if err != nil {
return err
}

// update parent directory
dir.RemoveFile(item)
return nil
}
67 changes: 67 additions & 0 deletions pkg/remove/parallel_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build linux
// +build linux

package remove

import (
"os"
"testing"

"github.com/dundee/gdu/v5/internal/testdir"
"github.com/dundee/gdu/v5/pkg/analyze"
"github.com/dundee/gdu/v5/pkg/fs"
"github.com/stretchr/testify/assert"
)

func TestRemoveItemFromDirParallelWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

err := os.Chmod("test_dir/nested", 0)
assert.Nil(t, err)
defer func() {
err = os.Chmod("test_dir/nested", 0755)
assert.Nil(t, err)
}()

dir := &analyze.Dir{
File: &analyze.File{
Name: "test_dir",
},
BasePath: ".",
}

subdir := &analyze.Dir{
File: &analyze.File{
Name: "nested",
Parent: dir,
},
}

err = RemoveItemFromDirParallel(dir, subdir)
assert.Contains(t, err.Error(), "permission denied")
}

func TestRemoveItemFromDirParallelWithErr2(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

err := os.Chmod("test_dir/nested/subnested", 0)
assert.Nil(t, err)
defer func() {
err = os.Chmod("test_dir/nested/subnested", 0755)
assert.Nil(t, err)
}()

analyzer := analyze.CreateAnalyzer()
dir := analyzer.AnalyzeDir(
"test_dir", func(_, _ string) bool { return false }, false,
).(*analyze.Dir)
analyzer.GetDone().Wait()
dir.UpdateStats(make(fs.HardLinkedItems))

subdir := dir.Files[0].(*analyze.Dir)

err = RemoveItemFromDirParallel(dir, subdir)
assert.Contains(t, err.Error(), "permission denied")
}
Loading
Loading