From a30296b476a57df9d32e1cb18d1db95bea800de0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 15 Feb 2023 15:21:27 -0500 Subject: 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 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- gopls/internal/lsp/filecache/filecache.go | 52 ++++++++++++++++++++++++++++--- 1 file 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) + } + } } } -- cgit v1.2.3