aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlan Donovan <adonovan@google.com>2023-02-15 15:21:27 -0500
committerAlan Donovan <adonovan@google.com>2023-02-16 17:09:37 +0000
commita30296b476a57df9d32e1cb18d1db95bea800de0 (patch)
tree0debcd29276c1bcf1500a211825294e7f18d9e9b
parent268cb0b82f51be725f8ec432c155a92075cd54c6 (diff)
downloadgolang-x-tools-a30296b476a57df9d32e1cb18d1db95bea800de0.tar.gz
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>
-rw-r--r--gopls/internal/lsp/filecache/filecache.go52
1 files changed, 48 insertions, 4 deletions
diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go
index 44485cb80..a0e63c45f 100644
--- a/gopls/internal/lsp/filecache/filecache.go
+++ b/gopls/internal/lsp/filecache/filecache.go
@@ -246,11 +246,14 @@ func gc(goplsDir string) {
// 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 @@ func gc(goplsDir string) {
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 @@ func gc(goplsDir string) {
}
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)
+ }
+ }
}
}