diff options
author | Alan Donovan <adonovan@google.com> | 2023-02-15 15:21:27 -0500 |
---|---|---|
committer | Alan Donovan <adonovan@google.com> | 2023-02-16 17:09:37 +0000 |
commit | a30296b476a57df9d32e1cb18d1db95bea800de0 (patch) | |
tree | 0debcd29276c1bcf1500a211825294e7f18d9e9b | |
parent | 268cb0b82f51be725f8ec432c155a92075cd54c6 (diff) | |
download | golang-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.go | 52 |
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) + } + } } } |