Skip to content

Performance regression: ~3x slowdown after commit c86b8604 #2249

@litenjacob

Description

@litenjacob

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 --noEmit

After 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 --noEmit

Both 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 CheckSourceFile is idempotent (checks typeChecked flag), 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions