github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/cache/cache.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cache 6 7 import ( 8 "context" 9 "crypto/sha256" 10 "fmt" 11 "go/ast" 12 "go/token" 13 "go/types" 14 "html/template" 15 "io/ioutil" 16 "os" 17 "reflect" 18 "sort" 19 "strconv" 20 "sync" 21 "sync/atomic" 22 "time" 23 24 "github.com/jhump/golang-x-tools/internal/event" 25 "github.com/jhump/golang-x-tools/internal/gocommand" 26 "github.com/jhump/golang-x-tools/internal/lsp/debug/tag" 27 "github.com/jhump/golang-x-tools/internal/lsp/source" 28 "github.com/jhump/golang-x-tools/internal/memoize" 29 "github.com/jhump/golang-x-tools/internal/span" 30 ) 31 32 func New(options func(*source.Options)) *Cache { 33 index := atomic.AddInt64(&cacheIndex, 1) 34 c := &Cache{ 35 id: strconv.FormatInt(index, 10), 36 fset: token.NewFileSet(), 37 options: options, 38 fileContent: map[span.URI]*fileHandle{}, 39 } 40 return c 41 } 42 43 type Cache struct { 44 id string 45 fset *token.FileSet 46 options func(*source.Options) 47 48 store memoize.Store 49 50 fileMu sync.Mutex 51 fileContent map[span.URI]*fileHandle 52 } 53 54 type fileHandle struct { 55 modTime time.Time 56 uri span.URI 57 bytes []byte 58 hash string 59 err error 60 61 // size is the file length as reported by Stat, for the purpose of 62 // invalidation. Probably we could just use len(bytes), but this is done 63 // defensively in case the definition of file size in the file system 64 // differs. 65 size int64 66 } 67 68 func (h *fileHandle) Saved() bool { 69 return true 70 } 71 72 func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { 73 return c.getFile(ctx, uri) 74 } 75 76 func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) { 77 fi, statErr := os.Stat(uri.Filename()) 78 if statErr != nil { 79 return &fileHandle{ 80 err: statErr, 81 uri: uri, 82 }, nil 83 } 84 85 c.fileMu.Lock() 86 fh, ok := c.fileContent[uri] 87 c.fileMu.Unlock() 88 89 // Check mtime and file size to infer whether the file has changed. This is 90 // an imperfect heuristic. Notably on some real systems (such as WSL) the 91 // filesystem clock resolution can be large -- 1/64s was observed. Therefore 92 // it's quite possible for multiple file modifications to occur within a 93 // single logical 'tick'. This can leave the cache in an incorrect state, but 94 // unfortunately we can't afford to pay the price of reading the actual file 95 // content here. Or to be more precise, reading would be a risky change and 96 // we don't know if we can afford it. 97 // 98 // We check file size in an attempt to reduce the probability of false cache 99 // hits. 100 if ok && fh.modTime.Equal(fi.ModTime()) && fh.size == fi.Size() { 101 return fh, nil 102 } 103 104 fh, err := readFile(ctx, uri, fi) 105 if err != nil { 106 return nil, err 107 } 108 c.fileMu.Lock() 109 c.fileContent[uri] = fh 110 c.fileMu.Unlock() 111 return fh, nil 112 } 113 114 // ioLimit limits the number of parallel file reads per process. 115 var ioLimit = make(chan struct{}, 128) 116 117 func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, error) { 118 select { 119 case ioLimit <- struct{}{}: 120 case <-ctx.Done(): 121 return nil, ctx.Err() 122 } 123 defer func() { <-ioLimit }() 124 125 ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename())) 126 _ = ctx 127 defer done() 128 129 data, err := ioutil.ReadFile(uri.Filename()) 130 if err != nil { 131 return &fileHandle{ 132 modTime: fi.ModTime(), 133 size: fi.Size(), 134 err: err, 135 }, nil 136 } 137 return &fileHandle{ 138 modTime: fi.ModTime(), 139 size: fi.Size(), 140 uri: uri, 141 bytes: data, 142 hash: hashContents(data), 143 }, nil 144 } 145 146 func (c *Cache) NewSession(ctx context.Context) *Session { 147 index := atomic.AddInt64(&sessionIndex, 1) 148 options := source.DefaultOptions().Clone() 149 if c.options != nil { 150 c.options(options) 151 } 152 s := &Session{ 153 cache: c, 154 id: strconv.FormatInt(index, 10), 155 options: options, 156 overlays: make(map[span.URI]*overlay), 157 gocmdRunner: &gocommand.Runner{}, 158 } 159 event.Log(ctx, "New session", KeyCreateSession.Of(s)) 160 return s 161 } 162 163 func (c *Cache) FileSet() *token.FileSet { 164 return c.fset 165 } 166 167 func (h *fileHandle) URI() span.URI { 168 return h.uri 169 } 170 171 func (h *fileHandle) Hash() string { 172 return h.hash 173 } 174 175 func (h *fileHandle) FileIdentity() source.FileIdentity { 176 return source.FileIdentity{ 177 URI: h.uri, 178 Hash: h.hash, 179 } 180 } 181 182 func (h *fileHandle) Read() ([]byte, error) { 183 return h.bytes, h.err 184 } 185 186 func hashContents(contents []byte) string { 187 return fmt.Sprintf("%x", sha256.Sum256(contents)) 188 } 189 190 var cacheIndex, sessionIndex, viewIndex int64 191 192 func (c *Cache) ID() string { return c.id } 193 func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() } 194 195 type packageStat struct { 196 id PackageID 197 mode source.ParseMode 198 file int64 199 ast int64 200 types int64 201 typesInfo int64 202 total int64 203 } 204 205 func (c *Cache) PackageStats(withNames bool) template.HTML { 206 var packageStats []packageStat 207 c.store.DebugOnlyIterate(func(k, v interface{}) { 208 switch k.(type) { 209 case packageHandleKey: 210 v := v.(*packageData) 211 if v.pkg == nil { 212 break 213 } 214 var typsCost, typInfoCost int64 215 if v.pkg.types != nil { 216 typsCost = typesCost(v.pkg.types.Scope()) 217 } 218 if v.pkg.typesInfo != nil { 219 typInfoCost = typesInfoCost(v.pkg.typesInfo) 220 } 221 stat := packageStat{ 222 id: v.pkg.m.ID, 223 mode: v.pkg.mode, 224 types: typsCost, 225 typesInfo: typInfoCost, 226 } 227 for _, f := range v.pkg.compiledGoFiles { 228 stat.file += int64(len(f.Src)) 229 stat.ast += astCost(f.File) 230 } 231 stat.total = stat.file + stat.ast + stat.types + stat.typesInfo 232 packageStats = append(packageStats, stat) 233 } 234 }) 235 var totalCost int64 236 for _, stat := range packageStats { 237 totalCost += stat.total 238 } 239 sort.Slice(packageStats, func(i, j int) bool { 240 return packageStats[i].total > packageStats[j].total 241 }) 242 html := "<table><thead><td>Name</td><td>total = file + ast + types + types info</td></thead>\n" 243 human := func(n int64) string { 244 return fmt.Sprintf("%.2f", float64(n)/(1024*1024)) 245 } 246 var printedCost int64 247 for _, stat := range packageStats { 248 name := stat.id 249 if !withNames { 250 name = "-" 251 } 252 html += fmt.Sprintf("<tr><td>%v (%v)</td><td>%v = %v + %v + %v + %v</td></tr>\n", name, stat.mode, 253 human(stat.total), human(stat.file), human(stat.ast), human(stat.types), human(stat.typesInfo)) 254 printedCost += stat.total 255 if float64(printedCost) > float64(totalCost)*.9 { 256 break 257 } 258 } 259 html += "</table>\n" 260 return template.HTML(html) 261 } 262 263 func astCost(f *ast.File) int64 { 264 if f == nil { 265 return 0 266 } 267 var count int64 268 ast.Inspect(f, func(_ ast.Node) bool { 269 count += 32 // nodes are pretty small. 270 return true 271 }) 272 return count 273 } 274 275 func typesCost(scope *types.Scope) int64 { 276 cost := 64 + int64(scope.Len())*128 // types.object looks pretty big 277 for i := 0; i < scope.NumChildren(); i++ { 278 cost += typesCost(scope.Child(i)) 279 } 280 return cost 281 } 282 283 func typesInfoCost(info *types.Info) int64 { 284 // Most of these refer to existing objects, with the exception of InitOrder, Selections, and Types. 285 cost := 24*len(info.Defs) + 286 32*len(info.Implicits) + 287 256*len(info.InitOrder) + // these are big, but there aren't many of them. 288 32*len(info.Scopes) + 289 128*len(info.Selections) + // wild guess 290 128*len(info.Types) + // wild guess 291 32*len(info.Uses) 292 return int64(cost) 293 }