From 05b3aef241f112ea98382a1a5cbb6814027570f8 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 13 Aug 2025 16:41:40 +0100 Subject: [PATCH 1/3] feat: optimize packagejson cache with concurrent-safe sync/atomic and SyncMap - Replaced sync.RWMutex with atomic.Bool for thread-safe readonly flag - Switched to collections.SyncMap for concurrent cache access - Updated getPackageJsonInfo to use new IsReadonly() method - Added SetReadonly method for efficient readonly state management - Enhanced Get/Set operations for improved performance and thread safety --- internal/module/resolver.go | 4 ++-- internal/packagejson/cache.go | 33 ++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 70b1b9afac..f277aeac24 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -1669,7 +1669,7 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string, onlyRecord Fields: packageJsonContent, }, } - if !r.resolver.packageJsonInfoCache.IsReadonly { + if !r.resolver.packageJsonInfoCache.IsReadonly() { r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) } r.affectingLocations = append(r.affectingLocations, packageJsonPath) @@ -1678,7 +1678,7 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string, onlyRecord if directoryExists && r.tracer != nil { r.tracer.write(diagnostics.File_0_does_not_exist.Format(packageJsonPath)) } - if !r.resolver.packageJsonInfoCache.IsReadonly { + if !r.resolver.packageJsonInfoCache.IsReadonly() { r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ PackageDirectory: packageDirectory, DirectoryExists: directoryExists, diff --git a/internal/packagejson/cache.go b/internal/packagejson/cache.go index 0736a260a4..86bfc97881 100644 --- a/internal/packagejson/cache.go +++ b/internal/packagejson/cache.go @@ -2,6 +2,7 @@ package packagejson import ( "sync" + "sync/atomic" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" @@ -121,9 +122,8 @@ func (p *InfoCacheEntry) GetDirectory() string { } type InfoCache struct { - mu sync.RWMutex - IsReadonly bool - cache map[tspath.Path]InfoCacheEntry + cache collections.SyncMap[tspath.Path, *InfoCacheEntry] + isReadonly atomic.Bool currentDirectory string useCaseSensitiveFileNames bool } @@ -136,22 +136,25 @@ func NewInfoCache(currentDirectory string, useCaseSensitiveFileNames bool) *Info } func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry { - p.mu.RLock() - defer p.mu.RUnlock() key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) - entry, ok := p.cache[key] - if !ok { - return nil + if value, ok := p.cache.Load(key); ok { + return value } - return &entry + return nil } func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) { - p.mu.Lock() - defer p.mu.Unlock() - key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) - if p.cache == nil { - p.cache = make(map[tspath.Path]InfoCacheEntry) + if p.isReadonly.Load() { + return } - p.cache[key] = *info + key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) + p.cache.Store(key, info) +} + +func (p *InfoCache) SetReadonly(readonly bool) { + p.isReadonly.Store(readonly) +} + +func (p *InfoCache) IsReadonly() bool { + return p.isReadonly.Load() } From abcd79f89b6daf2afda4a60f6060d3b36a66bfa3 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 13 Aug 2025 21:47:39 +0100 Subject: [PATCH 2/3] remove `isReadonly` from `InfoCache` --- internal/module/resolver.go | 14 +++++--------- internal/packagejson/cache.go | 13 ------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index f277aeac24..e3d27f2b9d 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -1669,21 +1669,17 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string, onlyRecord Fields: packageJsonContent, }, } - if !r.resolver.packageJsonInfoCache.IsReadonly() { - r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) - } + r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) r.affectingLocations = append(r.affectingLocations, packageJsonPath) return result } else { if directoryExists && r.tracer != nil { r.tracer.write(diagnostics.File_0_does_not_exist.Format(packageJsonPath)) } - if !r.resolver.packageJsonInfoCache.IsReadonly() { - r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ - PackageDirectory: packageDirectory, - DirectoryExists: directoryExists, - }) - } + r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ + PackageDirectory: packageDirectory, + DirectoryExists: directoryExists, + }) r.failedLookupLocations = append(r.failedLookupLocations, packageJsonPath) } return nil diff --git a/internal/packagejson/cache.go b/internal/packagejson/cache.go index 86bfc97881..188cd6dad8 100644 --- a/internal/packagejson/cache.go +++ b/internal/packagejson/cache.go @@ -2,7 +2,6 @@ package packagejson import ( "sync" - "sync/atomic" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" @@ -123,7 +122,6 @@ func (p *InfoCacheEntry) GetDirectory() string { type InfoCache struct { cache collections.SyncMap[tspath.Path, *InfoCacheEntry] - isReadonly atomic.Bool currentDirectory string useCaseSensitiveFileNames bool } @@ -144,17 +142,6 @@ func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry { } func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) { - if p.isReadonly.Load() { - return - } key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) p.cache.Store(key, info) } - -func (p *InfoCache) SetReadonly(readonly bool) { - p.isReadonly.Store(readonly) -} - -func (p *InfoCache) IsReadonly() bool { - return p.isReadonly.Load() -} From 4c00149a0808e8549c1b87224b95d8b054bee88d Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 13 Aug 2025 22:02:53 +0100 Subject: [PATCH 3/3] use `LoadOrStore` in `InfoCache::Set` --- internal/module/resolver.go | 4 ++-- internal/packagejson/cache.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index e3d27f2b9d..815c3ad62b 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -1669,14 +1669,14 @@ func (r *resolutionState) getPackageJsonInfo(packageDirectory string, onlyRecord Fields: packageJsonContent, }, } - r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) + result = r.resolver.packageJsonInfoCache.Set(packageJsonPath, result) r.affectingLocations = append(r.affectingLocations, packageJsonPath) return result } else { if directoryExists && r.tracer != nil { r.tracer.write(diagnostics.File_0_does_not_exist.Format(packageJsonPath)) } - r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ + _ = r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{ PackageDirectory: packageDirectory, DirectoryExists: directoryExists, }) diff --git a/internal/packagejson/cache.go b/internal/packagejson/cache.go index 188cd6dad8..7b72a4e3e9 100644 --- a/internal/packagejson/cache.go +++ b/internal/packagejson/cache.go @@ -141,7 +141,8 @@ func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry { return nil } -func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) { +func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) *InfoCacheEntry { key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) - p.cache.Store(key, info) + actual, _ := p.cache.LoadOrStore(key, info) + return actual }