Skip to content

Cache FS in LS #996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 5, 2025
Merged
5 changes: 5 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ func (api *API) Log(s string) {
api.options.Logger.Info(s)
}

// Log implements ProjectHost.
func (api *API) Trace(s string) {
api.options.Logger.Info(s)
}

// NewLine implements ProjectHost.
func (api *API) NewLine() string {
return api.host.NewLine()
Expand Down
40 changes: 33 additions & 7 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import (
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
)

//go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -output=project_stringer_generated.go
Expand Down Expand Up @@ -72,6 +74,7 @@ const (

type ProjectHost interface {
tsoptions.ParseConfigHost
module.ResolutionHost
NewLine() string
DefaultLibraryPath() string
TypingsInstaller() *TypingsInstaller
Expand Down Expand Up @@ -120,7 +123,7 @@ func typeAcquisitionChanged(opt1 *core.TypeAcquisition, opt2 *core.TypeAcquisiti
var _ compiler.CompilerHost = (*Project)(nil)

type Project struct {
host ProjectHost
host *projectHostWithCachedFS

name string
kind Kind
Expand Down Expand Up @@ -186,21 +189,23 @@ func NewInferredProject(compilerOptions *core.CompilerOptions, currentDirectory
}

func NewProject(name string, kind Kind, currentDirectory string, host ProjectHost) *Project {
cachedHost := newProjectHostWithCachedFS(host)

host.Log(fmt.Sprintf("Creating %sProject: %s, currentDirectory: %s", kind.String(), name, currentDirectory))
project := &Project{
host: host,
host: cachedHost,
name: name,
kind: kind,
currentDirectory: currentDirectory,
rootFileNames: &collections.OrderedMap[tspath.Path, string]{},
}
project.comparePathsOptions = tspath.ComparePathsOptions{
CurrentDirectory: currentDirectory,
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
UseCaseSensitiveFileNames: project.host.FS().UseCaseSensitiveFileNames(),
}
client := host.Client()
if host.IsWatchEnabled() && client != nil {
globMapper := createResolutionLookupGlobMapper(host)
client := project.host.Client()
if project.host.IsWatchEnabled() && client != nil {
globMapper := createResolutionLookupGlobMapper(project.host)
project.failedLookupsWatch = newWatchedFiles(project, lsproto.WatchKindCreate, globMapper, "failed lookup")
project.affectingLocationsWatch = newWatchedFiles(project, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, globMapper, "affecting location")
project.typingsFilesWatch = newWatchedFiles(project, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, globMapperForTypingsInstaller, "typings installer files")
Expand All @@ -210,6 +215,24 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos
return project
}

type projectHostWithCachedFS struct {
ProjectHost
fs *cachedvfs.FS
}

func newProjectHostWithCachedFS(host ProjectHost) *projectHostWithCachedFS {
newHost := &projectHostWithCachedFS{
ProjectHost: host,
fs: cachedvfs.From(host.FS()),
}
newHost.fs.DisableAndClearCache()
return newHost
}

func (p *projectHostWithCachedFS) FS() vfs.FS {
return p.fs
}

// FS implements compiler.CompilerHost.
func (p *Project) FS() vfs.FS {
return p.host.FS()
Expand Down Expand Up @@ -263,7 +286,7 @@ func (p *Project) NewLine() string {

// Trace implements compiler.CompilerHost.
func (p *Project) Trace(msg string) {
p.Log(msg)
p.host.Log(msg)
}

// GetDefaultLibraryPath implements compiler.CompilerHost.
Expand Down Expand Up @@ -461,6 +484,9 @@ func (p *Project) updateGraph() bool {
return false
}

p.host.fs.Enable()
defer p.host.fs.DisableAndClearCache()

start := time.Now()
p.Log("Starting updateGraph: Project: " + p.name)
var writeFileNames bool
Expand Down
4 changes: 4 additions & 0 deletions internal/project/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ func (s *Service) Log(msg string) {
s.options.Logger.Info(msg)
}

func (s *Service) Trace(msg string) {
s.Log(msg)
}

func (s *Service) HasLevel(level LogLevel) bool {
return s.options.Logger.HasLevel(level)
}
Expand Down
119 changes: 70 additions & 49 deletions internal/project/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ func TestService(t *testing.T) {
service, _ := projecttestutil.Setup(files, nil)
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/y.ts") == nil)
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

err := service.ChangeFile(
lsproto.VersionedTextDocumentIdentifier{
Expand Down Expand Up @@ -214,6 +216,8 @@ func TestService(t *testing.T) {
_, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts")
programBefore := project.GetProgram()
assert.Equal(t, len(programBefore.GetSourceFiles()), 2)
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

err := service.ChangeFile(
lsproto.VersionedTextDocumentIdentifier{
Expand Down Expand Up @@ -242,15 +246,16 @@ func TestService(t *testing.T) {
)
assert.NilError(t, err)

files["/home/projects/TS/p1/tsconfig.json"] = `{
err = host.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{
"compilerOptions": {
"noLib": true,
"module": "nodenext",
"strict": true,
},
"include": ["./**/*"]
}`
host.ReplaceFS(files)
}`, false)
assert.NilError(t, err)

err = service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeChanged,
Expand All @@ -270,23 +275,25 @@ func TestService(t *testing.T) {
t.Parallel()
t.Run("delete a file, close it, recreate it", func(t *testing.T) {
t.Parallel()
service, host := projecttestutil.Setup(defaultFiles, nil)
service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
files := maps.Clone(defaultFiles)
service, host := projecttestutil.Setup(files, nil)
service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
assert.Equal(t, service.SourceFileCount(), 2)
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

files := maps.Clone(defaultFiles)
delete(files, "/home/projects/TS/p1/src/x.ts")
host.ReplaceFS(files)
assert.NilError(t, host.FS().Remove("/home/projects/TS/p1/src/x.ts"))

service.CloseFile("/home/projects/TS/p1/src/x.ts")
assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil)
assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil)
assert.Equal(t, service.SourceFileCount(), 1)

files["/home/projects/TS/p1/src/x.ts"] = ``
host.ReplaceFS(files)
service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
err := host.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "", false)
assert.NilError(t, err)

service.OpenFile("/home/projects/TS/p1/src/x.ts", "", core.ScriptKindTS, "")
assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "")
assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil)
assert.Equal(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "")
Expand All @@ -300,19 +307,22 @@ func TestService(t *testing.T) {
files := maps.Clone(defaultFiles)
delete(files, "/home/projects/TS/p1/tsconfig.json")
service, host := projecttestutil.Setup(files, nil)
service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

delete(files, "/home/projects/TS/p1/src/x.ts")
host.ReplaceFS(files)
err := host.FS().Remove("/home/projects/TS/p1/src/x.ts")
assert.NilError(t, err)

service.CloseFile("/home/projects/TS/p1/src/x.ts")
assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil)
assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil)

files["/home/projects/TS/p1/src/x.ts"] = ``
host.ReplaceFS(files)
service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
err = host.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "", false)
assert.NilError(t, err)

service.OpenFile("/home/projects/TS/p1/src/x.ts", "", core.ScriptKindTS, "")
assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "")
assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil)
assert.Equal(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "")
Expand All @@ -338,6 +348,8 @@ func TestService(t *testing.T) {
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p2/src/index.ts", files["/home/projects/TS/p2/src/index.ts"].(string), core.ScriptKindTS, "")
assert.Equal(t, len(service.Projects()), 2)
// Avoid using initial file set after this point
files = nil //nolint:ineffassign
_, p1 := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts")
_, p2 := service.EnsureDefaultProjectForFile("/home/projects/TS/p2/src/index.ts")
assert.Equal(
Expand All @@ -361,6 +373,8 @@ func TestService(t *testing.T) {
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p2/src/index.ts", files["/home/projects/TS/p2/src/index.ts"].(string), core.ScriptKindTS, "")
assert.Equal(t, len(service.Projects()), 2)
// Avoid using initial file set after this point
files = nil //nolint:ineffassign
_, p1 := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts")
_, p2 := service.EnsureDefaultProjectForFile("/home/projects/TS/p2/src/index.ts")
x1 := p1.GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts")
Expand All @@ -375,15 +389,18 @@ func TestService(t *testing.T) {

t.Run("change open file", func(t *testing.T) {
t.Parallel()
service, host := projecttestutil.Setup(defaultFiles, nil)
service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
files := maps.Clone(defaultFiles)
service, host := projecttestutil.Setup(files, nil)
service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"].(string), core.ScriptKindTS, "")
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
_, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts")
programBefore := project.GetProgram()
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

err := host.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`, false)
assert.NilError(t, err)

files := maps.Clone(defaultFiles)
files["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;`
host.ReplaceFS(files)
assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeChanged,
Expand All @@ -396,14 +413,17 @@ func TestService(t *testing.T) {

t.Run("change closed program file", func(t *testing.T) {
t.Parallel()
service, host := projecttestutil.Setup(defaultFiles, nil)
service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
files := maps.Clone(defaultFiles)
service, host := projecttestutil.Setup(files, nil)
service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"].(string), core.ScriptKindTS, "")
_, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts")
programBefore := project.GetProgram()
// Avoid using initial file set after this point
files = nil //nolint:ineffassign

err := host.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`, false)
assert.NilError(t, err)

files := maps.Clone(defaultFiles)
files["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;`
host.ReplaceFS(files)
assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeChanged,
Expand Down Expand Up @@ -435,14 +455,14 @@ func TestService(t *testing.T) {
program := project.GetProgram()
assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)

filesCopy := maps.Clone(files)
filesCopy["/home/projects/TS/p1/tsconfig.json"] = `{
err := host.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{
"compilerOptions": {
"noLib": false,
"strict": true
}
}`
host.ReplaceFS(filesCopy)
}`, false)
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeChanged,
Expand Down Expand Up @@ -472,9 +492,9 @@ func TestService(t *testing.T) {
program := project.GetProgram()
assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0)

filesCopy := maps.Clone(files)
delete(filesCopy, "/home/projects/TS/p1/src/x.ts")
host.ReplaceFS(filesCopy)
err := host.FS().Remove("/home/projects/TS/p1/src/x.ts")
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeDeleted,
Expand Down Expand Up @@ -505,9 +525,9 @@ func TestService(t *testing.T) {
program := project.GetProgram()
assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0)

filesCopy := maps.Clone(files)
delete(filesCopy, "/home/projects/TS/p1/src/index.ts")
host.ReplaceFS(filesCopy)
err := host.FS().Remove("/home/projects/TS/p1/src/index.ts")
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeDeleted,
Expand Down Expand Up @@ -561,9 +581,9 @@ func TestService(t *testing.T) {
})

// Add the missing file
filesCopy := maps.Clone(files)
filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;`
host.ReplaceFS(filesCopy)
err := host.FS().WriteFile("/home/projects/TS/p1/src/y.ts", `export const y = 1;`, false)
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeCreated,
Expand Down Expand Up @@ -602,9 +622,9 @@ func TestService(t *testing.T) {
}))

// Add a new file through failed lookup watch
filesCopy := maps.Clone(files)
filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;`
host.ReplaceFS(filesCopy)
err := host.FS().WriteFile("/home/projects/TS/p1/src/z.ts", `export const z = 1;`, false)
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeCreated,
Expand Down Expand Up @@ -638,9 +658,10 @@ func TestService(t *testing.T) {
assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1)

// Add a new file through wildcard watch
filesCopy := maps.Clone(files)
filesCopy["/home/projects/TS/p1/src/a.ts"] = `const a = 1;`
host.ReplaceFS(filesCopy)

err := host.FS().WriteFile("/home/projects/TS/p1/src/a.ts", `const a = 1;`, false)
assert.NilError(t, err)

assert.NilError(t, service.OnWatchedFilesChanged(t.Context(), []*lsproto.FileEvent{
{
Type: lsproto.FileChangeTypeCreated,
Expand Down
4 changes: 0 additions & 4 deletions internal/testutil/projecttestutil/projecttestutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ func (p *ProjectServiceHost) Client() project.Client {
return p.ClientMock
}

func (p *ProjectServiceHost) ReplaceFS(files map[string]any) {
p.fs = bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/))
}

var _ project.ServiceHost = (*ProjectServiceHost)(nil)

func Setup(files map[string]any, testOptions *TestTypingsInstaller) (*project.Service, *ProjectServiceHost) {
Expand Down
Loading