Skip to content

Commit bff4645

Browse files
bartvandenende-wmcursoragentTwitchBronBron
authored
perf(ProjectManager): cache PathCollection per project in flushDocumentChanges (#1628)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Bronley Plumb <bronley@gmail.com>
1 parent e908cf1 commit bff4645

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/lsp/ProjectManager.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,41 @@ describe('ProjectManager', () => {
585585
});
586586

587587
describe('flushDocumentChanges', () => {
588+
it('reuses cached PathCollection across multiple flushes', async () => {
589+
fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ``);
590+
fsExtra.outputJsonSync(`${rootDir}/bsconfig.json`, {});
591+
await manager.syncProjects([workspaceSettings]);
592+
593+
//spy on PathCollection constructor to count how many are created
594+
const project = manager.projects[0];
595+
//first flush to warm the cache
596+
await manager['flushDocumentChanges']({
597+
actions: [{
598+
srcPath: s`${rootDir}/source/main.brs`,
599+
type: 'set',
600+
fileContents: 'sub main():end sub',
601+
allowStandaloneProject: true
602+
}]
603+
});
604+
605+
//grab the cached filterer
606+
const cachedFilterer = manager['projectFiltererCache'].get(project);
607+
expect(cachedFilterer).to.exist;
608+
609+
//second flush should reuse the same filterer instance
610+
await manager['flushDocumentChanges']({
611+
actions: [{
612+
srcPath: s`${rootDir}/source/main.brs`,
613+
type: 'set',
614+
fileContents: 'sub main2():end sub',
615+
allowStandaloneProject: true
616+
}]
617+
});
618+
619+
const cachedFilterer2 = manager['projectFiltererCache'].get(project);
620+
expect(cachedFilterer2).to.equal(cachedFilterer);
621+
});
622+
588623
it('does not crash when getting undefined back from projects', async () => {
589624
fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ``);
590625
fsExtra.outputJsonSync(`${rootDir}/project1/bsconfig.json`, {

src/lsp/ProjectManager.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ export class ProjectManager {
7272

7373
public busyStatusTracker = new BusyStatusTracker<LspProject>();
7474

75+
/**
76+
* Cache for PathCollection instances per project. Avoids recreating PathCollection
77+
* on every document flush, which is wasteful since file patterns only change when a project is reloaded.
78+
*/
79+
private projectFiltererCache = new WeakMap<LspProject, PathCollection>();
80+
81+
/**
82+
* Get or create a cached PathCollection for the given project.
83+
* The filterer is invalidated when the project is removed and garbage collected.
84+
*/
85+
private getProjectFilterer(project: LspProject): PathCollection {
86+
let filterer = this.projectFiltererCache.get(project);
87+
if (!filterer) {
88+
filterer = new PathCollection({
89+
rootDir: project.rootDir,
90+
globs: project.filePatterns
91+
});
92+
this.projectFiltererCache.set(project, filterer);
93+
}
94+
return filterer;
95+
}
96+
7597
/**
7698
* Apply all of the queued document changes. This should only be called as a result of the documentManager flushing changes, and never called manually
7799
* @param event the document changes that have occurred since the last time we applied
@@ -101,10 +123,7 @@ export class ProjectManager {
101123
//wait for this project to finish activating
102124
await project.whenActivated();
103125

104-
const filterer = new PathCollection({
105-
rootDir: project.rootDir,
106-
globs: project.filePatterns
107-
});
126+
const filterer = this.getProjectFilterer(project);
108127
// only include files that are applicable to this specific project (still allow deletes to flow through since they're cheap)
109128
const projectActions = actions.filter(action => {
110129
return (

0 commit comments

Comments
 (0)