gopls/internal/lsp/filecache: purge empty directories
This change causes the GC thread to attempt to remove
all directories in the cache; only the empty directories
are actually removed. This is a one-time act about a minute
after startup, which should be sufficient to prevent runaway
directory proliferation.
Tested interactively.
Fixes golang/go#57915
Change-Id: Ic950c706ad8862ac735b8ef0fa263df917c6e13e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/468539
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go
index 44485cb8..a0e63c4 100644
--- a/gopls/internal/lsp/filecache/filecache.go
+++ b/gopls/internal/lsp/filecache/filecache.go
@@ -246,11 +246,14 @@
// tests) are able to make progress sweeping garbage.
//
// (gopls' caches should never actually get this big in
- // practise: the example mentioned above resulted from a bug
+ // practice: the example mentioned above resulted from a bug
// that caused filecache to fail to delete any files.)
const debug = false
+ // Names of all directories found in first pass; nil thereafter.
+ dirs := make(map[string]bool)
+
for {
// Enumerate all files in the cache.
type item struct {
@@ -260,9 +263,15 @@
var files []item
var total int64 // bytes
_ = filepath.Walk(goplsDir, func(path string, stat os.FileInfo, err error) error {
- // TODO(adonovan): opt: also collect empty directories,
- // as they typically occupy around 1KB.
- if err == nil && !stat.IsDir() {
+ if err != nil {
+ return nil // ignore errors
+ }
+ if stat.IsDir() {
+ // Collect (potentially empty) directories.
+ if dirs != nil {
+ dirs[path] = true
+ }
+ } else {
// Unconditionally delete files we haven't used in ages.
// (We do this here, not in the second loop, so that we
// perform age-based collection even in short-lived processes.)
@@ -303,5 +312,40 @@
}
time.Sleep(period)
+
+ // Once only, delete all directories.
+ // This will succeed only for the empty ones,
+ // and ensures that stale directories (whose
+ // files have been deleted) are removed eventually.
+ // They don't take up much space but they do slow
+ // down the traversal.
+ //
+ // We do this after the sleep to minimize the
+ // race against Set, which may create a directory
+ // that is momentarily empty.
+ //
+ // (Test processes don't live that long, so
+ // this may not be reached on the CI builders.)
+ if dirs != nil {
+ dirnames := make([]string, 0, len(dirs))
+ for dir := range dirs {
+ dirnames = append(dirnames, dir)
+ }
+ dirs = nil
+
+ // Descending length order => children before parents.
+ sort.Slice(dirnames, func(i, j int) bool {
+ return len(dirnames[i]) > len(dirnames[j])
+ })
+ var deleted int
+ for _, dir := range dirnames {
+ if os.Remove(dir) == nil { // ignore error
+ deleted++
+ }
+ }
+ if debug {
+ log.Printf("deleted %d empty directories", deleted)
+ }
+ }
}
}