563 lines
20 KiB
Go
563 lines
20 KiB
Go
package project
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/ls"
|
|
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/project/ata"
|
|
"github.com/microsoft/typescript-go/internal/project/dirty"
|
|
"github.com/microsoft/typescript-go/internal/project/logging"
|
|
"github.com/microsoft/typescript-go/internal/sourcemap"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
|
|
)
|
|
|
|
type Snapshot struct {
|
|
id uint64
|
|
parentId uint64
|
|
refCount atomic.Int32
|
|
|
|
// Session options are immutable for the server lifetime,
|
|
// so can be a pointer.
|
|
sessionOptions *SessionOptions
|
|
toPath func(fileName string) tspath.Path
|
|
converters *lsconv.Converters
|
|
|
|
// Immutable state, cloned between snapshots
|
|
fs *SnapshotFS
|
|
ProjectCollection *ProjectCollection
|
|
ConfigFileRegistry *ConfigFileRegistry
|
|
AutoImports *autoimport.Registry
|
|
autoImportsWatch *WatchedFiles[map[tspath.Path]string]
|
|
compilerOptionsForInferredProjects *core.CompilerOptions
|
|
userPreferences lsutil.UserPreferences
|
|
|
|
builderLogs *logging.LogTree
|
|
apiError error
|
|
}
|
|
|
|
// NewSnapshot initializes a snapshot with refCount 1.
|
|
// The caller is responsible for calling Deref when done.
|
|
func NewSnapshot(
|
|
id uint64,
|
|
fs *SnapshotFS,
|
|
sessionOptions *SessionOptions,
|
|
configFileRegistry *ConfigFileRegistry,
|
|
compilerOptionsForInferredProjects *core.CompilerOptions,
|
|
userPreferences lsutil.UserPreferences,
|
|
autoImports *autoimport.Registry,
|
|
autoImportsWatch *WatchedFiles[map[tspath.Path]string],
|
|
toPath func(fileName string) tspath.Path,
|
|
) *Snapshot {
|
|
s := &Snapshot{
|
|
id: id,
|
|
|
|
sessionOptions: sessionOptions,
|
|
toPath: toPath,
|
|
|
|
fs: fs,
|
|
ConfigFileRegistry: configFileRegistry,
|
|
ProjectCollection: &ProjectCollection{toPath: toPath, openFiles: openFilePaths(fs.overlays)},
|
|
compilerOptionsForInferredProjects: compilerOptionsForInferredProjects,
|
|
userPreferences: userPreferences,
|
|
AutoImports: autoImports,
|
|
autoImportsWatch: autoImportsWatch,
|
|
}
|
|
s.refCount.Store(1)
|
|
s.converters = lsconv.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap)
|
|
return s
|
|
}
|
|
|
|
func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project {
|
|
return s.ProjectCollection.GetDefaultProject(uri.Path(s.UseCaseSensitiveFileNames()))
|
|
}
|
|
|
|
func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project {
|
|
fileName := uri.FileName()
|
|
path := s.toPath(fileName)
|
|
// TODO!! sheetal may be change this to handle symlinks!!
|
|
return s.ProjectCollection.GetProjectsContainingFile(path)
|
|
}
|
|
|
|
func (s *Snapshot) GetFile(fileName string) FileHandle {
|
|
return s.fs.GetFile(fileName)
|
|
}
|
|
|
|
func (s *Snapshot) LSPLineMap(fileName string) *lsconv.LSPLineMap {
|
|
if file := s.fs.GetFile(fileName); file != nil {
|
|
return file.LSPLineMap()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo {
|
|
if file := s.fs.GetFile(fileName); file != nil {
|
|
return file.ECMALineInfo()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Snapshot) GetPreferences(activeFile string) lsutil.UserPreferences {
|
|
return s.userPreferences
|
|
}
|
|
|
|
func (s *Snapshot) UserPreferences() lsutil.UserPreferences {
|
|
return s.userPreferences
|
|
}
|
|
|
|
func (s *Snapshot) Converters() *lsconv.Converters {
|
|
return s.converters
|
|
}
|
|
|
|
func (s *Snapshot) AutoImportRegistry() *autoimport.Registry {
|
|
return s.AutoImports
|
|
}
|
|
|
|
func (s *Snapshot) ID() uint64 {
|
|
return s.id
|
|
}
|
|
|
|
func (s *Snapshot) UseCaseSensitiveFileNames() bool {
|
|
return s.fs.fs.UseCaseSensitiveFileNames()
|
|
}
|
|
|
|
func (s *Snapshot) ReadFile(fileName string) (string, bool) {
|
|
handle := s.GetFile(fileName)
|
|
if handle == nil {
|
|
return "", false
|
|
}
|
|
return handle.Content(), true
|
|
}
|
|
|
|
func (s *Snapshot) DirectoryExists(path string) bool {
|
|
return s.fs.fs.DirectoryExists(path)
|
|
}
|
|
|
|
func (s *Snapshot) FileExists(path string) bool {
|
|
return s.fs.fs.FileExists(path)
|
|
}
|
|
|
|
func (s *Snapshot) GetDirectories(path string) []string {
|
|
return s.fs.fs.GetAccessibleEntries(path).Directories
|
|
}
|
|
|
|
func (s *Snapshot) ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string {
|
|
return vfsmatch.ReadDirectory(s.fs.fs, currentDir, path, extensions, excludes, includes, depth)
|
|
}
|
|
|
|
type APISnapshotRequest struct {
|
|
OpenProjects *collections.Set[string]
|
|
CloseProjects *collections.Set[tspath.Path]
|
|
OpenFiles *collections.Set[lsproto.DocumentUri]
|
|
CloseFiles *collections.Set[tspath.Path]
|
|
}
|
|
|
|
type ProjectTreeRequest struct {
|
|
// If null, all project trees need to be loaded, otherwise only those that are referenced
|
|
referencedProjects *collections.Set[tspath.Path]
|
|
}
|
|
|
|
func (p *ProjectTreeRequest) IsAllProjects() bool {
|
|
return p.referencedProjects == nil
|
|
}
|
|
|
|
func (p *ProjectTreeRequest) IsProjectReferenced(projectID tspath.Path) bool {
|
|
return p.referencedProjects.Has(projectID)
|
|
}
|
|
|
|
func (p *ProjectTreeRequest) Projects() []tspath.Path {
|
|
if p.referencedProjects == nil {
|
|
return nil
|
|
}
|
|
return slices.Collect(maps.Keys(p.referencedProjects.Keys()))
|
|
}
|
|
|
|
type ResourceRequest struct {
|
|
// Documents are URIs that were requested by the client.
|
|
// The new snapshot should ensure projects for these URIs have loaded programs.
|
|
Documents []lsproto.DocumentUri
|
|
// ConfiguredProjectDocuments are URIs for which configured projects should be loaded
|
|
// (if disableSolutionSearching/disableReferencedProjectLoad settings allow),
|
|
// but no inferred project should be created if no configured project is found.
|
|
// This is used by cross-project operations like find-all-references.
|
|
ConfiguredProjectDocuments []lsproto.DocumentUri
|
|
// Update requested Projects.
|
|
// this is used when we want to get LS and from all the Projects the file can be part of
|
|
Projects []tspath.Path
|
|
// Update and ensure project trees that reference the projects
|
|
// This is used to compute the solution and project tree so that
|
|
// we can find references across all the projects in the solution irrespective of which project is open
|
|
ProjectTree *ProjectTreeRequest
|
|
// AutoImports is the document URI for which auto imports should be prepared.
|
|
AutoImports lsproto.DocumentUri
|
|
}
|
|
|
|
type SnapshotChange struct {
|
|
ResourceRequest
|
|
reason UpdateReason
|
|
// fileChanges are the changes that have occurred since the last snapshot.
|
|
fileChanges FileChangeSummary
|
|
// compilerOptionsForInferredProjects is the compiler options to use for inferred projects.
|
|
// It should only be set the value in the next snapshot should be changed. If nil, the
|
|
// value from the previous snapshot will be copied to the new snapshot.
|
|
compilerOptionsForInferredProjects *core.CompilerOptions
|
|
newConfig *lsutil.UserPreferences
|
|
// ataChanges contains ATA-related changes to apply to projects in the new snapshot.
|
|
ataChanges map[tspath.Path]*ATAStateChange
|
|
apiRequest *APISnapshotRequest
|
|
// cleanDiskCache triggers cleaning of cached disk files not referenced by any open project.
|
|
cleanDiskCache bool
|
|
}
|
|
|
|
// ATAStateChange represents a change to a project's ATA state.
|
|
type ATAStateChange struct {
|
|
ProjectID tspath.Path
|
|
// TypingsInfo is the new typings info for the project.
|
|
TypingsInfo *ata.TypingsInfo
|
|
// TypingsFiles is the new list of typing files for the project.
|
|
TypingsFiles []string
|
|
// TypingsFilesToWatch is the new list of typing files to watch for changes.
|
|
TypingsFilesToWatch []string
|
|
Logs *logging.LogTree
|
|
}
|
|
|
|
func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays map[tspath.Path]*Overlay, session *Session) *Snapshot {
|
|
var logger *logging.LogTree
|
|
|
|
// Print in-progress logs immediately if cloning fails
|
|
if session.options.LoggingEnabled {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
session.logger.Log(logger.String())
|
|
panic(r)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if session.options.LoggingEnabled {
|
|
logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d", s.id))
|
|
getDetails := func() string {
|
|
details := ""
|
|
if len(change.Documents) != 0 {
|
|
details += fmt.Sprintf(" Documents: %v", change.Documents)
|
|
}
|
|
if len(change.ConfiguredProjectDocuments) != 0 {
|
|
details += fmt.Sprintf(" ConfiguredProjectDocuments: %v", change.ConfiguredProjectDocuments)
|
|
}
|
|
if len(change.Projects) != 0 {
|
|
details += fmt.Sprintf(" Projects: %v", change.Projects)
|
|
}
|
|
if change.ProjectTree != nil {
|
|
details += fmt.Sprintf(" ProjectTree: %v", change.ProjectTree.Projects())
|
|
}
|
|
return details
|
|
}
|
|
switch change.reason {
|
|
case UpdateReasonDidOpenFile:
|
|
logger.Logf("Reason: DidOpenFile - %s", change.fileChanges.Opened)
|
|
case UpdateReasonDidCloseFile:
|
|
logger.Logf("Reason: DidCloseFile - %v", change.fileChanges.Closed)
|
|
case UpdateReasonDidChangeCompilerOptionsForInferredProjects:
|
|
logger.Logf("Reason: DidChangeCompilerOptionsForInferredProjects")
|
|
case UpdateReasonRequestedLanguageServicePendingChanges:
|
|
logger.Logf("Reason: RequestedLanguageService (pending file changes) - %v", getDetails())
|
|
case UpdateReasonRequestedLanguageServiceProjectNotLoaded:
|
|
logger.Logf("Reason: RequestedLanguageService (project not loaded) - %v", getDetails())
|
|
case UpdateReasonRequestedLanguageServiceForFileNotOpen:
|
|
logger.Logf("Reason: RequestedLanguageService (file not open) - %v", getDetails())
|
|
case UpdateReasonRequestedLanguageServiceProjectDirty:
|
|
logger.Logf("Reason: RequestedLanguageService (project dirty) - %v", getDetails())
|
|
case UpdateReasonRequestedLoadProjectTree:
|
|
logger.Logf("Reason: RequestedLoadProjectTree - %v", getDetails())
|
|
case UpdateReasonIdleCleanDiskCache:
|
|
logger.Logf("Reason: IdleCleanDiskCache")
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
fs := newSnapshotFSBuilder(session.fs.fs, s.fs.overlays, overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, session.options.PositionEncoding, s.toPath)
|
|
if change.fileChanges.HasExcessiveWatchEvents() {
|
|
invalidateStart := time.Now()
|
|
if change.fileChanges.InvalidateAll {
|
|
fs.invalidateCache()
|
|
logger.Logf("InvalidateAll: invalidated file cache in %v", time.Since(invalidateStart))
|
|
} else if !fs.watchChangesOverlapCache(change.fileChanges) {
|
|
// All watch changes/deletes are files we haven't seen; should be irrelevant to us (probably an external tool's build or something)
|
|
change.fileChanges.Changed = collections.Set[lsproto.DocumentUri]{}
|
|
change.fileChanges.Deleted = collections.Set[lsproto.DocumentUri]{}
|
|
} else if change.fileChanges.IncludesWatchChangeOutsideNodeModules {
|
|
fs.invalidateCache()
|
|
logger.Logf("Excessive watch changes detected, invalidated file cache in %v", time.Since(invalidateStart))
|
|
} else {
|
|
fs.invalidateNodeModulesCache()
|
|
logger.Logf("npm install detected, invalidated node_modules cache in %v", time.Since(invalidateStart))
|
|
}
|
|
} else {
|
|
change.fileChanges = fs.expandAndFilterWatchEvents(change.fileChanges)
|
|
change.fileChanges = s.fs.expandRealpathAliases(change.fileChanges)
|
|
fs.markDirtyFiles(change.fileChanges)
|
|
change.fileChanges = fs.convertOpenAndCloseToChanges(change.fileChanges)
|
|
}
|
|
|
|
compilerOptionsForInferredProjects := s.compilerOptionsForInferredProjects
|
|
if change.compilerOptionsForInferredProjects != nil {
|
|
// !!! mark inferred projects as dirty?
|
|
compilerOptionsForInferredProjects = change.compilerOptionsForInferredProjects
|
|
}
|
|
|
|
// Compute effective customConfigFileName from user preferences
|
|
customConfigFileName := s.ConfigFileRegistry.customConfigFileName
|
|
if change.newConfig != nil {
|
|
customConfigFileName = change.newConfig.CustomConfigFileName
|
|
}
|
|
|
|
newSnapshotID := session.snapshotID.Add(1)
|
|
projectCollectionBuilder := newProjectCollectionBuilder(
|
|
ctx,
|
|
newSnapshotID,
|
|
fs,
|
|
s.ProjectCollection,
|
|
s.ConfigFileRegistry,
|
|
s.ProjectCollection.apiState,
|
|
compilerOptionsForInferredProjects,
|
|
s.sessionOptions,
|
|
customConfigFileName,
|
|
session.parseCache,
|
|
session.extendedConfigCache,
|
|
session.client,
|
|
)
|
|
|
|
if len(change.ataChanges) != 0 {
|
|
projectCollectionBuilder.DidUpdateATAState(change.ataChanges, logger.Fork("DidUpdateATAState"))
|
|
}
|
|
|
|
projectCollectionBuilder.DidChangeCustomConfigFileName(logger.Fork("DidChangeCustomConfigFileName"))
|
|
|
|
if !change.fileChanges.IsEmpty() {
|
|
projectCollectionBuilder.DidChangeFiles(change.fileChanges, logger.Fork("DidChangeFiles"))
|
|
}
|
|
|
|
var apiError error
|
|
if change.apiRequest != nil {
|
|
apiError = projectCollectionBuilder.HandleAPIRequest(change.apiRequest, logger.Fork("HandleAPIRequest"))
|
|
}
|
|
|
|
for _, uri := range change.Documents {
|
|
projectCollectionBuilder.DidRequestFile(uri, false /*configuredProjectsOnly*/, logger.Fork("DidRequestFile"))
|
|
}
|
|
|
|
for _, uri := range change.ConfiguredProjectDocuments {
|
|
projectCollectionBuilder.DidRequestFile(uri, true /*configuredProjectsOnly*/, logger.Fork("DidRequestFile (optional)"))
|
|
}
|
|
|
|
for _, projectId := range change.Projects {
|
|
projectCollectionBuilder.DidRequestProject(projectId, logger.Fork("DidRequestProject"))
|
|
}
|
|
|
|
if change.ProjectTree != nil {
|
|
projectCollectionBuilder.DidRequestProjectTrees(change.ProjectTree, logger.Fork("DidRequestProjectTrees"))
|
|
}
|
|
|
|
projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger)
|
|
|
|
projectsWithNewProgramStructure := make(map[tspath.Path]bool)
|
|
for _, project := range projectCollection.Projects() {
|
|
if project.ProgramLastUpdate == newSnapshotID && project.ProgramUpdateKind != ProgramUpdateKindCloned {
|
|
projectsWithNewProgramStructure[project.configFilePath] = project.ProgramUpdateKind == ProgramUpdateKindNewFiles
|
|
}
|
|
}
|
|
|
|
// Clean cached disk files not touched by any open project on file open, close, delete,
|
|
// or when explicitly requested (e.g. by an idle timer).
|
|
shouldCleanDiskCache := change.cleanDiskCache ||
|
|
change.fileChanges.Opened != "" ||
|
|
change.fileChanges.Reopened != "" ||
|
|
change.fileChanges.Closed.Len() > 0 ||
|
|
change.fileChanges.Deleted.Len() > 0
|
|
if shouldCleanDiskCache {
|
|
// The set of seen files can change only if a program was constructed (not cloned) during this snapshot.
|
|
// When cleanDiskCache is explicitly set, always attempt cleaning.
|
|
if len(projectsWithNewProgramStructure) > 0 || change.cleanDiskCache {
|
|
cleanFilesStart := time.Now()
|
|
removedFiles := 0
|
|
fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
|
|
for _, project := range projectCollection.Projects() {
|
|
if project.host != nil && project.host.sourceFS.SeenFile(entry.Key()) {
|
|
return true
|
|
}
|
|
}
|
|
entry.Delete()
|
|
removedFiles++
|
|
return true
|
|
})
|
|
if session.options.LoggingEnabled {
|
|
logger.Logf("Removed %d cached file(s) in %v", removedFiles, time.Since(cleanFilesStart))
|
|
}
|
|
}
|
|
}
|
|
|
|
config := s.userPreferences
|
|
if change.newConfig != nil {
|
|
config = *change.newConfig
|
|
}
|
|
|
|
autoImportHost := newAutoImportRegistryCloneHost(
|
|
projectCollection,
|
|
session.parseCache,
|
|
fs,
|
|
s.sessionOptions.CurrentDirectory,
|
|
s.toPath,
|
|
)
|
|
openFiles := make(map[tspath.Path]string, len(overlays))
|
|
for path, overlay := range overlays {
|
|
openFiles[path] = overlay.FileName()
|
|
}
|
|
prepareAutoImports := tspath.Path("")
|
|
if change.ResourceRequest.AutoImports != "" {
|
|
prepareAutoImports = change.ResourceRequest.AutoImports.Path(s.UseCaseSensitiveFileNames())
|
|
}
|
|
oldAutoImports := s.AutoImports
|
|
if oldAutoImports == nil {
|
|
oldAutoImports = autoimport.NewRegistry(s.toPath, s.userPreferences)
|
|
}
|
|
var autoImportsWatch *WatchedFiles[map[tspath.Path]string]
|
|
autoImports, err := oldAutoImports.Clone(ctx, autoimport.RegistryChange{
|
|
RequestedFile: prepareAutoImports,
|
|
OpenFiles: openFiles,
|
|
Changed: change.fileChanges.Changed,
|
|
Created: change.fileChanges.Created,
|
|
Deleted: change.fileChanges.Deleted,
|
|
RebuiltPrograms: projectsWithNewProgramStructure,
|
|
UserPreferences: change.newConfig,
|
|
}, autoImportHost, logger.Fork("UpdateAutoImports"))
|
|
if err == nil {
|
|
autoImportsWatch = s.autoImportsWatch.Clone(autoImports.NodeModulesDirectories())
|
|
}
|
|
|
|
snapshotFS, _ := fs.Finalize()
|
|
newSnapshot := NewSnapshot(
|
|
newSnapshotID,
|
|
snapshotFS,
|
|
s.sessionOptions,
|
|
nil,
|
|
compilerOptionsForInferredProjects,
|
|
config,
|
|
autoImports,
|
|
autoImportsWatch,
|
|
s.toPath,
|
|
)
|
|
newSnapshot.parentId = s.id
|
|
newSnapshot.ProjectCollection = projectCollection
|
|
newSnapshot.ConfigFileRegistry = configFileRegistry
|
|
newSnapshot.builderLogs = logger
|
|
newSnapshot.apiError = apiError
|
|
|
|
for _, project := range newSnapshot.ProjectCollection.Projects() {
|
|
if project.Program != nil {
|
|
session.programCounter.Ref(project.Program)
|
|
if project.ProgramLastUpdate == newSnapshotID {
|
|
// If the program was updated during this clone, the project and its host are new
|
|
// and still retain references to the builder. Freezing clears the builder reference
|
|
// so it's GC'd and to ensure the project can't access any data not already in the
|
|
// snapshot during use. This is pretty kludgy, but it's an artifact of Program design:
|
|
// Program has a single host, which is expected to implement a full vfs.FS, among
|
|
// other things. That host is *mostly* only used during program *construction*, but a
|
|
// few methods may get exercised during program *use*. So, our compiler host is allowed
|
|
// to access caches and perform mutating effects (like acquire referenced project
|
|
// config files) during snapshot building, and then we call `freeze` to ensure those
|
|
// mutations don't happen afterwards. In the future, we might improve things by
|
|
// separating what it takes to build a program from what it takes to use a program,
|
|
// and only pass the former into NewProgram instead of retaining it indefinitely.
|
|
project.host.freeze(snapshotFS, newSnapshot.ConfigFileRegistry)
|
|
}
|
|
}
|
|
}
|
|
for _, config := range newSnapshot.ConfigFileRegistry.configs {
|
|
if config.commandLine != nil && config.commandLine.ConfigFile != nil {
|
|
for _, file := range config.commandLine.ConfigFile.ExtendedSourceFiles {
|
|
session.extendedConfigCache.AddOwner(newSnapshot.toPath(file), newSnapshot.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
autoImportHost.Dispose()
|
|
|
|
logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start))
|
|
return newSnapshot
|
|
}
|
|
|
|
// ref increments the snapshot's reference count, preventing it from being
|
|
// disposed until a corresponding Deref is called. The snapshot must still
|
|
// be alive (refCount > 0) when ref is called. Only the project Session
|
|
// should call ref(), and it should be done while holding session.snapshotMu.
|
|
func (s *Snapshot) ref() {
|
|
if s.refCount.Add(1) <= 1 {
|
|
panic(fmt.Sprintf("snapshot %d: ref on disposed snapshot, parentId=%d", s.id, s.parentId))
|
|
}
|
|
}
|
|
|
|
// tryRef attempts to increment the snapshot's reference count. If the
|
|
// snapshot is already disposed (refCount == 0), it returns false without
|
|
// modifying the count. On success the caller must eventually call Deref.
|
|
func (s *Snapshot) tryRef() bool {
|
|
for {
|
|
rc := s.refCount.Load()
|
|
if rc <= 0 {
|
|
return false
|
|
}
|
|
if s.refCount.CompareAndSwap(rc, rc+1) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deref decrements the snapshot's reference count. When the count reaches
|
|
// zero, the snapshot is disposed and its resources are released.
|
|
func (s *Snapshot) Deref(session *Session) {
|
|
rc := s.refCount.Add(-1)
|
|
if rc < 0 {
|
|
panic(fmt.Sprintf("snapshot %d: ref count below zero, parentId=%d", s.id, s.parentId))
|
|
}
|
|
if rc == 0 {
|
|
s.dispose(session)
|
|
}
|
|
}
|
|
|
|
func (s *Snapshot) dispose(session *Session) {
|
|
for _, project := range s.ProjectCollection.Projects() {
|
|
if project.Program != nil && session.programCounter.Deref(project.Program) {
|
|
// This program is no longer referenced by any snapshot.
|
|
// Mark its checker pool as discarded so its idle-cleanup timer stops
|
|
// keeping the pool alive, allowing the pool and any idle checkers it
|
|
// still references to be reclaimed when the pool is garbage-collected.
|
|
if project.checkerPool != nil {
|
|
project.checkerPool.Discard()
|
|
}
|
|
for _, file := range project.Program.SourceFiles() {
|
|
session.parseCache.Deref(NewParseCacheKey(file.ParseOptions(), file.Hash, file.ScriptKind))
|
|
}
|
|
for _, file := range project.Program.DuplicateSourceFiles() {
|
|
session.parseCache.Deref(NewParseCacheKey(file.ParseOptions, file.Hash, file.ScriptKind))
|
|
}
|
|
}
|
|
}
|
|
for _, config := range s.ConfigFileRegistry.configs {
|
|
if config.commandLine != nil {
|
|
for _, file := range config.commandLine.ExtendedSourceFiles() {
|
|
session.extendedConfigCache.Release(session.toPath(file), s.id)
|
|
}
|
|
}
|
|
}
|
|
}
|