Skip to content

Conversation

@kbrilla
Copy link
Contributor

@kbrilla kbrilla commented Feb 8, 2026

PR: perf(language-service): progressive project initialization

Description

Replaces the blocking project.refreshDiagnostics() during project initialization with a progressive background approach. Instead of sending all diagnostic refresh events at once, open files are now processed in configurable chunks with event loop yields between them, keeping the IDE responsive during Angular project startup.

Problem

When a project finishes loading (ProjectLoadingFinishEvent), the language server calls:

this.runGlobalAnalysisForNewlyLoadedProject(project);
project.refreshDiagnostics(); // triggers diagnostics for ALL open files at once

The project.refreshDiagnostics() call sends a ProjectsUpdatedInBackgroundEvent which triggers diagnostic computation for all open files. While the existing sendPendingDiagnostics already yields between individual files, the initial event processing can still cause a burst of work that degrades IDE responsiveness during startup.

Solution

Replaced project.refreshDiagnostics() with a new initializeProjectProgressively() method that:

  1. Processes files in chunks (default: 10 files per chunk)
  2. Yields to the event loop between chunks via setImmediateP()
  3. Sends diagnostics immediately per file — users see results appearing progressively
  4. Cancels gracefully if:
    • The project is closed during initialization
    • A new diagnostics request arrives (e.g., user starts typing)
    • Another progressive init is started (e.g., project reloads)
Before (blocking refresh):
  Analysis → [refreshDiagnostics: ALL FILES EVENT] → sendPendingDiagnostics
              (single burst of work scheduled)

After (progressive):
  Analysis → [chunk 1] → yield → [chunk 2] → yield → ... → Done
              ↑                    ↑
      (UI responsive, LSP requests served between chunks)

Cancellation Integration

Progressive initialization integrates with the existing diagnostics system:

  • When triggerDiagnostics() is called (file opened/changed), any ongoing progressive init is cancelled via progressiveInitCancelled flag
  • This prevents stale startup diagnostics from overwriting fresher results
  • The pattern mirrors the existing diagnosticsTimeout cancellation logic

Changes

File Change
vscode-ng-language-service/server/src/session.ts Added progressiveInitCancelled field, initializeProjectProgressively() method, cancellation in triggerDiagnostics(), replaced project.refreshDiagnostics() in enableLanguageServiceForProject()

Impact

  • UI remains responsive during project startup
  • Diagnostics appear progressively — first results visible within seconds
  • No change to final state — all open files get diagnostics, same as before
  • Minimal code change — single file, no API changes

Testing

  • All language service tests pass (modern + legacy): //packages/language-service/...
  • All LSP integration tests pass: //vscode-ng-language-service/integration/lsp:test
  • All server unit tests pass: //vscode-ng-language-service/server/src/tests:test
  • Tests implicitly validate responsiveness (would timeout if event loop blocked)

Breaking Changes

None. This is an internal optimization that doesn't change observable behavior.

Related

Complements:


AI Disclosure

This PR was developed using Claude Opus 4 AI assistant under human orchestration and review by @kbrilla.

Integration

See #66967 for integration with Pull-Based Diagnostics, which optimizes progressive init to use workspace/diagnostic/refresh instead of computing and pushing diagnostics when the client supports pull-based diagnostics.

Replace project.refreshDiagnostics() with progressive background processing
during project initialization. Open files are now processed in configurable
chunks (default: 10) with event loop yields between chunks, keeping the IDE
responsive during Angular project startup.

Progressive initialization provides:
- Immediate feedback: diagnostics appear file-by-file
- Responsiveness: event loop not blocked between chunks
- Cancellation: stops if project closes or user starts typing
@pullapprove pullapprove bot requested a review from atscott February 8, 2026 19:50
@angular-robot angular-robot bot added area: performance Issues related to performance area: language-service Issues related to Angular's VS Code language service labels Feb 8, 2026
@ngbot ngbot bot modified the milestone: Backlog Feb 8, 2026
kbrilla added a commit to kbrilla/angular that referenced this pull request Feb 8, 2026
…agnostics

When pull-based diagnostics (LSP 3.17) is active, progressive project init
now sends a workspace/diagnostic/refresh notification instead of computing
and pushing diagnostics for each open file. This avoids wasted computation
since the pull-based client would discard pushed diagnostics anyway.

Falls back to the chunked push-based approach when pull diagnostics is not
supported by the client.

Depends on:
- PR angular#66700 (Pull-Based Diagnostics)
- PR angular#66966 (Progressive Project Initialization)
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been looking into this and it seems initializeProjectProgressively does almost exactly what the existing sendPendingDiagnostics already does, so adding a completely new method does not seem strictly necessary.

Here is a breakdown of the overlap I see between the two methods:

sendPendingDiagnostics already yields: The PR description mentions yielding to the event loop. However, the existing method already yields using await setImmediateP() after every single file.

Chunking regression: The PR's chunk size of 10 means it will process 10 files before yielding. This actually blocks the event loop for longer stretches than the existing code, which yields every 1 file. If computing diagnostics is heavy per file, yielding per file is more responsive.

Duplicated cancellation logic: This introduces progressiveInitCancelled to cancel the loop if new typing occurs. This basically duplicates the diagnosticsTimeout cancellation already built into sendPendingDiagnostics and triggerDiagnostics.

Latency vs Debounce: project.refreshDiagnostics() (the old way) emits an event that gets routed to triggerDiagnostics(), which comes with a 300ms debounce. The new progressive init runs immediately upon project initialization.

If the main goal is to provide immediate feedback without the 300ms debounce, could we simply skip triggerDiagnostics and call sendPendingDiagnostics directly with all open files?

If you truly believe chunking 10 files at a time is better for throughput than yielding on every file, we should probably just modify the existing sendPendingDiagnostics to accept a chunk size, rather than duplicating the evaluation and cancellation loops. Please also provide details about why this is better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: language-service Issues related to Angular's VS Code language service area: performance Issues related to performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants