You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The BrighterScript language server works well for single-project and small multi-project workspaces.
But as workspace scale increases, for example a monorepo with 30-60+ bsconfig.json files in a single VS Code workspace, certain design choices that are reasonable at smaller scale begin to show cumulative overhead, leading to significantly elevated CPU usage, out-of-memory errors, and excessive battery drain on local development machines.
This ticket captures our analysis, and a set of targeted, low-risk improvements to address this.
Root Causes Identified
File-change event storms
Every file-system event triggers onDidChangeWatchedFiles synchronously. In large workspaces, rapid successive events - e.g. from git checkout, bulk saves, or build tool output - each trigger a full processing cycle including PathFilterer rebuilds and project-level handleFileChanges calls. This means a burst of 50 events results in 50 independent processing cycles, each fanning out to all N projects
Unbounded parallel project activation syncProjects uses Promise.all to activate every discovered project simultaneously. When 30-60+ projects are discovered, all of them parse files, resolve plugins, and validate in parallel, creating a CPU spike that can saturate all cores and leave the system unresponsive during startup.
Redundant PathCollection creation flushDocumentChanges creates a new PathCollection instance for every project on every document flush (~150ms after each edit). Each construction compiles glob patterns into regex matchers via micromatch.matcher(). Since a project's file patterns only change on reload, this produces identical matchers every time, and redundant work that compounds with project count.
Proposed improvements
Rather than pursuing large architectural changes (lazy project loading, shared AST caches, declaration boundaries), which would be a significant long-term effort, the below 3 proposed improvements target the highest-impact multipliers within the current architecture:
Debounce onDidChangeWatchedFiles feat(LanguageServer): debounce onDidChangeWatchedFiles events #1626
Accumulate file-change events in a buffer and flush them as a single batch after a short configurable delay (e.g. 300ms). This collapses burst scenarios (e.g. git checkout generating dozens of events) into one processing cycle instead of one per event.
Before
After
50 rapid events × N projects = 50N processing cycles
1 batched flush × N projects = N processing cycles
Concurrency-limit project activation feat(ProjectManager): limit concurrency of project activation #1627
Replace Promise.all in syncProjects with a worker-pool pattern that limits how many projects activate concurrently. All projects still activate, but the CPU load is spread over time rather than spiking all at once, keeping the system responsive during startup.
Before
After
N projects activate in parallel → CPU spike
N projects activate in waves of 3 → smoother resource usage
Cache PathCollection per project perf(ProjectManager): cache PathCollection per project in flushDocumentChanges #1628
Use a WeakMap to lazily create and cache PathCollection instances per project. File patterns only change when a project reloads, so the cached filterer stays valid for the project's lifetime and is automatically garbage-collected when the project is removed.
Before
After
New PathCollection compiled per project per flush
One PathCollection per project, reused until project reloads
Context
The BrighterScript language server works well for single-project and small multi-project workspaces.
But as workspace scale increases, for example a monorepo with 30-60+ bsconfig.json files in a single VS Code workspace, certain design choices that are reasonable at smaller scale begin to show cumulative overhead, leading to significantly elevated CPU usage, out-of-memory errors, and excessive battery drain on local development machines.
This ticket captures our analysis, and a set of targeted, low-risk improvements to address this.
Root Causes Identified
Every file-system event triggers
onDidChangeWatchedFilessynchronously. In large workspaces, rapid successive events - e.g. from git checkout, bulk saves, or build tool output - each trigger a full processing cycle includingPathFiltererrebuilds and project-level handleFileChanges calls. This means a burst of 50 events results in 50 independent processing cycles, each fanning out to all N projectssyncProjectsusesPromise.allto activate every discovered project simultaneously. When 30-60+ projects are discovered, all of them parse files, resolve plugins, and validate in parallel, creating a CPU spike that can saturate all cores and leave the system unresponsive during startup.flushDocumentChangescreates a newPathCollectioninstance for every project on every document flush (~150ms after each edit). Each construction compiles glob patterns into regex matchers viamicromatch.matcher(). Since a project's file patterns only change on reload, this produces identical matchers every time, and redundant work that compounds with project count.Proposed improvements
Rather than pursuing large architectural changes (lazy project loading, shared AST caches, declaration boundaries), which would be a significant long-term effort, the below 3 proposed improvements target the highest-impact multipliers within the current architecture:
onDidChangeWatchedFilesfeat(LanguageServer): debounce onDidChangeWatchedFiles events #1626
Accumulate file-change events in a buffer and flush them as a single batch after a short configurable delay (e.g. 300ms). This collapses burst scenarios (e.g. git checkout generating dozens of events) into one processing cycle instead of one per event.
feat(ProjectManager): limit concurrency of project activation #1627
Replace
Promise.allinsyncProjectswith a worker-pool pattern that limits how many projects activate concurrently. All projects still activate, but the CPU load is spread over time rather than spiking all at once, keeping the system responsive during startup.PathCollectionper projectperf(ProjectManager): cache PathCollection per project in flushDocumentChanges #1628
Use a
WeakMapto lazily create and cachePathCollectioninstances per project. File patterns only change when a project reloads, so the cached filterer stays valid for the project's lifetime and is automatically garbage-collected when the project is removed.PathCollectioncompiled per project per flushPathCollectionper project, reused until project reloads