Skip to content

Commit

Permalink
feat: delete directory items in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
dundee committed Apr 14, 2024
1 parent 1cf03c0 commit 506dfb8
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 2 deletions.
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 @@ type Flags struct {
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 @@ func (a *App) createUI() (UI, error) {
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
2 changes: 1 addition & 1 deletion pkg/analyze/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,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 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 {

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

View check run for this annotation

Codecov / codecov/patch

pkg/remove/parallel.go#L30

Added line #L30 was not covered by tests
// 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#L32-L33

Added lines #L32 - L33 were not covered by tests
}
}
wait.Done()
}(file.GetPath())
}

wait.Wait()

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/remove/parallel.go#L44-L45

Added lines #L44 - L45 were not covered by tests
default:
}

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

// update parent directory
dir.RemoveFile(item)
return nil
}
42 changes: 42 additions & 0 deletions pkg/remove/parallel_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//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/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")
}
69 changes: 69 additions & 0 deletions pkg/remove/parallel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package remove

import (
"testing"

"github.com/stretchr/testify/assert"

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

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

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

err := RemoveItemFromDirParallel(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 TestRemoveDirParallel(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

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.Nil(t, err)
}
5 changes: 5 additions & 0 deletions tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ func (ui *UI) SetChangeCwdFn(fn func(string) error) {
ui.changeCwdFn = fn
}

// SetDeleteInParallel sets the flag to delete files in parallel
func (ui *UI) SetDeleteInParallel() {
ui.remover = remove.RemoveItemFromDirParallel
}

// StartUILoop starts tview application
func (ui *UI) StartUILoop() error {
return ui.app.Run()
Expand Down
78 changes: 78 additions & 0 deletions tui/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,29 @@ func TestDeleteSelected(t *testing.T) {
assert.NoDirExists(t, "test_dir/nested")
}

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

ui := getAnalyzedPathMockedApp(t, false, true, false)
ui.done = make(chan struct{})
ui.SetDeleteInParallel()

assert.Equal(t, 1, ui.table.GetRowCount())

ui.table.Select(0, 0)

ui.deleteSelected(false)

<-ui.done

for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
f()
}

assert.NoDirExists(t, "test_dir/nested")
}

func TestDeleteSelectedInBackground(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand All @@ -354,6 +377,31 @@ func TestDeleteSelectedInBackground(t *testing.T) {
assert.NoDirExists(t, "test_dir/nested")
}

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

ui := getAnalyzedPathMockedApp(t, true, true, false)
ui.remover = testanalyze.RemoveItemFromDirWithSleep
ui.done = make(chan struct{})
ui.SetDeleteInBackground()
ui.SetDeleteInParallel()

assert.Equal(t, 1, ui.table.GetRowCount())

ui.table.Select(0, 0)

ui.deleteSelected(false)

<-ui.done

for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
f()
}

assert.NoDirExists(t, "test_dir/nested")
}

func TestDeleteSelectedInBackgroundBW(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down Expand Up @@ -568,6 +616,36 @@ func TestDeleteMarkedInBackgroundWithStorage(t *testing.T) {
assert.NoFileExists(t, "test_dir/nested/file2")
}

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

ui := getAnalyzedPathMockedApp(t, false, true, false)
ui.SetAnalyzer(analyze.CreateStoredAnalyzer("/tmp/badger"))
ui.SetDeleteInBackground()
ui.SetDeleteInParallel()

assert.Equal(t, 1, ui.table.GetRowCount())

ui.fileItemSelected(0, 0) // nested

ui.markedRows[1] = struct{}{} // subnested
ui.markedRows[2] = struct{}{} // file2

ui.deleteMarked(false)

<-ui.done // wait for deletion of subnested
<-ui.done // wait for deletion of file2

for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
f()
}

assert.DirExists(t, "test_dir/nested")
assert.NoDirExists(t, "test_dir/nested/subnested")
assert.NoFileExists(t, "test_dir/nested/file2")
}

func TestDeleteMarkedInBackgroundWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down

0 comments on commit 506dfb8

Please sign in to comment.