-
Notifications
You must be signed in to change notification settings - Fork 749
Description
Summary
Commit c86b8604 ("Make Program diagnostic API clearer", PR #2197) seems to have introduced a significant performance regression, causing type-checking to be ~3x slower on large codebases.
Affected Versions
- Last fast version:
@typescript/native-preview@7.0.0-dev.20251203.1 - First slow version:
@typescript/native-preview@7.0.0-dev.20251204.1
Reproduction
Minimal reproduction using vscode
# Clone VS Code (shallow clone is sufficient)
git clone --depth 1 https://github.com/microsoft/vscode.git /tmp/vscode
cd /tmp/vscode
npm install --ignore-scripts
# Install both affected and unaffected tsgo versions from npm
npm install -g @typescript/native-preview@7.0.0-dev.20251203.1 @typescript/native-preview@7.0.0-dev.20251204.1
# Test fast unaffected version - ~4s
time npx --no-install @typescript/native-preview@7.0.0-dev.20251203.1 --project src/tsconfig.json --noEmit
# Test slow affected version - ~10s
time npx --no-install @typescript/native-preview@7.0.0-dev.20251204.1 --project src/tsconfig.json --noEmitAfter updating to 20251204.1, the same command takes ~3x longer.
Building from source (to verify exact commits)
# Clone VS Code (shallow clone is sufficient)
git clone --depth 1 https://github.com/microsoft/vscode.git /tmp/vscode
# Clone typescript-go
git clone https://github.com/microsoft/typescript-go.git /tmp/typescript-go
cd /tmp/typescript-go
# Build at commit BEFORE the regression (parent of offending commit)
git checkout c86b8604^
go build -ldflags="-s -w" -trimpath -tags release -o /tmp/tsgo-before ./cmd/tsgo
# Build at the offending commit
git checkout c86b8604
go build -ldflags="-s -w" -trimpath -tags release -o /tmp/tsgo-after ./cmd/tsgo
# Test BEFORE (fast) - ~4s
time /tmp/tsgo-before --project /tmp/vscode/src/tsconfig.json --noEmit
# Test AFTER (slow) - ~10s
time /tmp/tsgo-after --project /tmp/vscode/src/tsconfig.json --noEmitBoth versions produce identical output (19,100 lines), confirming they perform the same type-checking work - only the performance differs.
AI-generated analysis
Root Cause Analysis
The regression was introduced by removing the parallel CheckSourceFiles pre-pass.
Before (fast)
The old getDiagnosticsHelper called CheckSourceFiles(ctx, nil) once before collecting diagnostics. This function checked all files in parallel using ForEachCheckerParallel:
// REMOVED in c86b8604
func (p *Program) CheckSourceFiles(ctx context.Context, files []*ast.SourceFile) {
p.checkerPool.ForEachCheckerParallel(ctx, func(_ int, checker *checker.Checker) {
for file := range p.checkerPool.Files(checker) {
if files == nil || slices.Contains(files, file) {
checker.CheckSourceFile(ctx, file)
}
}
})
}After (slow)
The new collectDiagnostics function iterates through files sequentially:
func (p *Program) collectDiagnostics(ctx context.Context, sourceFile *ast.SourceFile,
collect func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic {
var result []*ast.Diagnostic
if sourceFile != nil {
result = collect(ctx, sourceFile)
} else {
for _, file := range p.files {
result = append(result, collect(ctx, file)...) // Sequential!
}
}
return SortAndDeduplicateDiagnostics(result)
}Each call to getSemanticDiagnosticsForFile now triggers individual file checking via GetCheckerForFile, losing the parallel speedup.
Environment Used for Testing
- Go 1.25.5
- macOS darwin/arm64 (Apple Silicon)
- VS Code repo at HEAD (shallow clone, no dependencies installed)
Suggested Fix
Restore parallel file checking before diagnostic collection when sourceFile is nil, while keeping the cleaner API introduced by the commit.
Proposed patch
Add a checkSourceFilesParallel method and call it before collecting diagnostics:
// checkSourceFilesParallel checks all source files in parallel using all available checkers.
// This is a performance optimization that ensures all files are type-checked before
// collecting diagnostics, allowing work to be distributed across multiple checkers.
func (p *Program) checkSourceFilesParallel(ctx context.Context) {
if pool, ok := p.checkerPool.(*checkerPool); ok {
pool.ForEachCheckerParallel(func(_ int, c *checker.Checker) {
for file := range pool.Files(c) {
c.CheckSourceFile(ctx, file)
}
})
}
}Then modify the diagnostic collection functions to call this before sequential collection:
func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
// When checking all files, pre-check them in parallel for better performance.
if sourceFile == nil {
p.checkSourceFilesParallel(ctx)
if ctx.Err() != nil {
return nil
}
}
return p.collectDiagnostics(ctx, sourceFile, p.getSemanticDiagnosticsForFile)
}Apply the same pattern to GetSuggestionDiagnostics, GetDeclarationDiagnostics, and GetSemanticDiagnosticsWithoutNoEmitFiltering.
Why this works
- The parallel pre-check distributes type-checking work across all available checkers concurrently
- Since
CheckSourceFileis idempotent (checkstypeCheckedflag), the subsequent sequential collection is fast - Single-file operations (
sourceFile != nil) remain unaffected - The cleaner API from the original commit is preserved
Verified fix performance (VS Code repo, 3 runs each)
| Version | Run 1 | Run 2 | Run 3 | Average |
|---|---|---|---|---|
| Slow (c86b860) | 10.4s | 10.1s | 10.2s | 10.2s |
| With fix | 3.30s | 3.25s | 3.35s | 3.30s |
| Original fast (c86b860^) | 3.27s | 3.35s | 3.32s | 3.31s |
The fix fully restores the original performance.
The AI seems to have been able to suggest a fix that mitigates the performance regression, but as I have no idea about its integrity nor any CLA signed, I figured it's better to just raise the issue and have you look into it.