Skip to content

Commit ffe5292

Browse files
Let the session own what a change reaches
A pull kept the incremental view of each project's program in a map on the server, keyed by project, and worked out when to rebuild it by comparing program pointers. That is state the session already knows how to keep: it belongs to a program, it is replaced when the program is, and it should go when the project does. It now sits on the project beside the checker pool, and the collection builder rolls it forward where it already discards the old pool. Rolling it forward keeps what the old program worked out and drops the program it worked it out from, which the map could not do: a map entry held the previous program until the next pull replaced it, so a project's last two programs were reachable at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 28b977b commit ffe5292

8 files changed

Lines changed: 98 additions & 94 deletions

File tree

tsc/internal/execute/incremental/program.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ type TestingData struct {
7272
UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind
7373
}
7474

75+
// WithoutProgram returns what a later program needs in order to work out what a change reached,
76+
// and nothing else. A caller holding a whole Program for that would hold its program, and every
77+
// type reachable from it, until the next one is built.
78+
func (p *Program) WithoutProgram() *Program {
79+
if p == nil {
80+
return nil
81+
}
82+
return &Program{snapshot: p.snapshot, host: p.host}
83+
}
84+
7585
func (p *Program) GetTestingData() *TestingData {
7686
return p.testingData
7787
}

tsc/internal/lsp/server.go

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,24 @@ func NewServer(opts *ServerOptions) *Server {
6363
}
6464

6565
s := &Server{
66-
r: opts.In,
67-
w: opts.Out,
68-
stderr: opts.Err,
69-
requestQueue: newDynamicQueue[*lsproto.RequestMessage](),
70-
outgoingQueue: newDynamicQueue[*lsproto.Message](),
71-
pendingClientRequests: make(map[jsonrpc.ID]pendingClientRequest),
72-
pendingServerRequests: make(map[jsonrpc.ID]chan *lsproto.ResponseMessage),
73-
cwd: opts.Cwd,
74-
fs: opts.FS,
75-
defaultLibraryPath: opts.DefaultLibraryPath,
76-
typingsLocation: opts.TypingsLocation,
77-
parseCache: opts.ParseCache,
78-
npmInstall: opts.NpmInstall,
79-
spawn: opts.Spawn,
80-
startWatchdog: opts.SetParentProcessID,
81-
initComplete: make(chan struct{}),
82-
progressDelay: opts.ProgressDelay,
83-
workspaceDiagnostics: newWorkspaceDiagnosticsCache(),
84-
workspaceDiagnosticsPrograms: newWorkspaceDiagnosticsPrograms(),
66+
r: opts.In,
67+
w: opts.Out,
68+
stderr: opts.Err,
69+
requestQueue: newDynamicQueue[*lsproto.RequestMessage](),
70+
outgoingQueue: newDynamicQueue[*lsproto.Message](),
71+
pendingClientRequests: make(map[jsonrpc.ID]pendingClientRequest),
72+
pendingServerRequests: make(map[jsonrpc.ID]chan *lsproto.ResponseMessage),
73+
cwd: opts.Cwd,
74+
fs: opts.FS,
75+
defaultLibraryPath: opts.DefaultLibraryPath,
76+
typingsLocation: opts.TypingsLocation,
77+
parseCache: opts.ParseCache,
78+
npmInstall: opts.NpmInstall,
79+
spawn: opts.Spawn,
80+
startWatchdog: opts.SetParentProcessID,
81+
initComplete: make(chan struct{}),
82+
progressDelay: opts.ProgressDelay,
83+
workspaceDiagnostics: newWorkspaceDiagnosticsCache(),
8584
}
8685
s.logger = newLogger(s)
8786

@@ -257,10 +256,6 @@ type Server struct {
257256
// produced the result id a client holds for each file.
258257
workspaceDiagnostics *workspaceDiagnosticsCache
259258

260-
// workspaceDiagnosticsPrograms remembers what each project looked like at the last pull, so a
261-
// pull re-checks only the files a change reached.
262-
workspaceDiagnosticsPrograms *workspaceDiagnosticsPrograms
263-
264259
workspaceDiagnosticsRegistrationMu sync.Mutex
265260
workspaceDiagnosticsRegistered bool
266261
}

tsc/internal/lsp/workspacediagnostics.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ func (s *Server) computeWorkspaceDiagnostics(ctx context.Context, params *lsprot
9898

9999
if run.collected {
100100
s.workspaceDiagnostics.retain(&run.reported)
101-
s.workspaceDiagnosticsPrograms.retain(&run.projects)
102101
if s.logger.IsVerbose() {
103102
stats := s.workspaceDiagnostics.stats()
104103
s.logger.Logf("workspace diagnostics: reported %d files, cached %d files across %d projects",
@@ -123,8 +122,6 @@ type workspaceDiagnosticsRun struct {
123122
previous map[lsproto.DocumentUri]string
124123
// Documents already covered, so a file in several projects is reported once.
125124
reported collections.Set[lsproto.DocumentUri]
126-
// Projects this pull was responsible for, so the ones it no longer covers can be let go of.
127-
projects collections.Set[tspath.Path]
128125

129126
// Reports not yet flushed; without a partial result token this holds all of them.
130127
pending []workspaceDiagnosticReport
@@ -253,7 +250,7 @@ func (r *workspaceDiagnosticsRun) checkProject(snapshot *project.Snapshot, pf wo
253250
}
254251
// Ask through the incremental view, so a change is re-checked where it landed rather than
255252
// across the whole project.
256-
program := r.server.workspaceDiagnosticsPrograms.forProject(pf.project.Id(), pf.languageService.GetProgram())
253+
program := snapshot.IncrementalProgram(pf.project)
257254
reports := pf.languageService.WorkspaceDiagnosticsForProject(r.ctx, program, files)
258255
if r.ctx.Err() != nil {
259256
return false
@@ -270,8 +267,6 @@ func (r *workspaceDiagnosticsRun) checkProject(snapshot *project.Snapshot, pf wo
270267
// emitProject hands a finished project's reports to the client and remembers which program version
271268
// produced each result id, so the next pull can skip the file.
272269
func (r *workspaceDiagnosticsRun) emitProject(pf workspaceDiagnosticsProject) {
273-
// Recorded here rather than while checking, since emitting is what runs in project order.
274-
r.projects.Add(pf.project.Id())
275270
for j, report := range pf.reports {
276271
if pf.files[j] != nil {
277272
r.filesDone++

tsc/internal/lsp/workspacediagnosticsincremental.go

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package project
2+
3+
import (
4+
"sync"
5+
6+
"github.com/microsoft/TypeScript/tsc/internal/compiler"
7+
"github.com/microsoft/TypeScript/tsc/internal/execute/incremental"
8+
)
9+
10+
// incrementalState carries what a project learned about which files a change reaches from one of
11+
// its programs to the next, so a pull re-checks only the files an edit affected.
12+
//
13+
// It is built on the first pull that asks, not when the program is, because building it walks every
14+
// file in the program and most programs are never pulled. Like the checker pool it is held by
15+
// pointer, so the snapshots that share a program share what it has built.
16+
type incrementalState struct {
17+
mu sync.Mutex
18+
// What the previous program left behind, holding no program of its own.
19+
previous *incremental.Program
20+
current *incremental.Program
21+
}
22+
23+
// get returns the incremental view of the program, building it from the previous program's
24+
// bookkeeping the first time it is asked for.
25+
func (s *incrementalState) get(program *compiler.Program) *incremental.Program {
26+
if s == nil {
27+
// A project built before it had any state to carry; nothing to chain from.
28+
return incremental.NewProgram(program, nil, nil, nil, false)
29+
}
30+
s.mu.Lock()
31+
defer s.mu.Unlock()
32+
if s.current == nil {
33+
s.current = incremental.NewProgram(program, s.previous, nil, nil, false)
34+
// The new view has taken what it needs; holding the old one keeps a program alive.
35+
s.previous = nil
36+
}
37+
return s.current
38+
}
39+
40+
// next returns the state a replacement program starts from. It keeps what this one worked out and
41+
// drops the program it worked it out from, which is the largest thing a project holds.
42+
func (s *incrementalState) next() *incrementalState {
43+
if s == nil {
44+
return &incrementalState{}
45+
}
46+
s.mu.Lock()
47+
defer s.mu.Unlock()
48+
previous := s.current
49+
if previous == nil {
50+
previous = s.previous
51+
}
52+
return &incrementalState{previous: previous.WithoutProgram()}
53+
}

tsc/internal/project/project.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ type Project struct {
8282
contentMapperWatchedFiles *collections.Set[tspath.Path]
8383

8484
checkerPool *checkerPool
85+
// incremental carries what a change reaches from one program to the next; see incrementalState.
86+
incremental *incrementalState
8587

8688
// installedTypingsInfo is the value of `project.ComputeTypingsInfo()` that was
8789
// used during the most recently completed typings installation.
@@ -184,6 +186,7 @@ func NewProject(
184186
Kind: kind,
185187
currentDirectory: currentDirectory,
186188
dirty: true,
189+
incremental: &incrementalState{},
187190
}
188191

189192
project.configFilePath = tspath.ToPath(configFileName, currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames())
@@ -312,6 +315,7 @@ func (p *Project) Clone() *Project {
312315
contentMapperWatchedFiles: p.contentMapperWatchedFiles,
313316

314317
checkerPool: p.checkerPool,
318+
incremental: p.incremental,
315319

316320
installedTypingsInfo: p.installedTypingsInfo,
317321
typingsFiles: p.typingsFiles,

tsc/internal/project/projectcollectionbuilder.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,7 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo
12991299
oldHost := project.host
13001300
oldProgram := project.Program
13011301
oldCheckerPool := project.checkerPool
1302+
oldIncremental := project.incremental
13021303
project.host = newCompilerHost(project.currentDirectory, project, b, logger.Fork("CompilerHost"))
13031304
result := project.CreateProgram()
13041305
var watchedFiles []string
@@ -1341,6 +1342,9 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo
13411342
if oldCheckerPool != nil {
13421343
oldCheckerPool.Discard()
13431344
}
1345+
// Carries what the old program worked out about its files, without carrying the
1346+
// program. Built here rather than on first use so the old one can be let go of now.
1347+
project.incremental = oldIncremental.next()
13441348
})
13451349
})
13461350
}

tsc/internal/project/snapshot.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/microsoft/TypeScript/tsc/internal/collections"
1414
"github.com/microsoft/TypeScript/tsc/internal/contentmapper"
1515
"github.com/microsoft/TypeScript/tsc/internal/core"
16+
"github.com/microsoft/TypeScript/tsc/internal/execute/incremental"
1617
"github.com/microsoft/TypeScript/tsc/internal/ls"
1718
"github.com/microsoft/TypeScript/tsc/internal/ls/autoimport"
1819
"github.com/microsoft/TypeScript/tsc/internal/ls/lsconv"
@@ -315,6 +316,13 @@ func (s *Snapshot) ReleaseCheckingPool(project *Project) bool {
315316
return project.checkerPool.releaseCheckingPool()
316317
}
317318

319+
// IncrementalProgram returns a project's program together with the record of which files a change
320+
// since the previous program reached, so a caller checking the project can skip the files it did
321+
// not. Built on first use, and shared by every snapshot holding the same program.
322+
func (s *Snapshot) IncrementalProgram(project *Project) *incremental.Program {
323+
return project.incremental.get(project.Program)
324+
}
325+
318326
func (s *Snapshot) OpenProjects() []*Project {
319327
var open []*Project
320328
for _, project := range s.ProjectCollection.Projects() {

0 commit comments

Comments
 (0)