Files
kjol/tools/tsgo/internal/compiler/checkerpool.go
2026-07-09 16:50:43 -04:00

168 lines
5.2 KiB
Go

package compiler
import (
"context"
"slices"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tracing"
)
// CheckerPool is implemented by the project system to provide checkers with
// request-scoped lifetime and reclamation. It returns a checker and a release
// function that must be called when the caller is done with the checker.
// The returned checker must not be accessed concurrently; each acquisition is exclusive.
// If file is non-nil, the pool may use it as an affinity hint to return the same
// checker for the same file across calls.
type CheckerPool interface {
GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
}
type checkerPool struct {
program *Program
tracing *tracing.Tracing
createCheckersOnce sync.Once
checkers []*checker.Checker
locks []*sync.Mutex
fileAssociations map[*ast.SourceFile]*checker.Checker
}
var _ CheckerPool = (*checkerPool)(nil)
func newCheckerPool(program *Program) *checkerPool {
return newCheckerPoolWithTracing(program, nil)
}
func newCheckerPoolWithTracing(program *Program, tr *tracing.Tracing) *checkerPool {
checkerCount := 4
if program.SingleThreaded() {
checkerCount = 1
} else if c := program.Options().Checkers; c != nil {
checkerCount = *c
}
checkerCount = max(min(checkerCount, len(program.files), 256), 1)
pool := &checkerPool{
program: program,
checkers: make([]*checker.Checker, checkerCount),
locks: make([]*sync.Mutex, checkerCount),
tracing: tr,
}
return pool
}
// GetChecker implements CheckerPool. When file is non-nil, returns the checker
// associated with that file; otherwise returns the first checker.
func (p *checkerPool) GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
if file != nil {
return p.getCheckerForFileExclusive(ctx, file)
}
p.createCheckers()
c := p.checkers[0]
p.locks[0].Lock()
return c, sync.OnceFunc(func() {
p.locks[0].Unlock()
})
}
// getCheckerForFileNonExclusive returns the checker for the given file without locking.
// This is only safe when the caller guarantees no concurrent access to the same checker,
// e.g. for read-only operations like obtaining an emit resolver.
func (p *checkerPool) getCheckerForFileNonExclusive(file *ast.SourceFile) (*checker.Checker, func()) {
p.createCheckers()
return p.fileAssociations[file], noop
}
func (p *checkerPool) getCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
p.createCheckers()
c := p.fileAssociations[file]
idx := slices.Index(p.checkers, c)
p.locks[idx].Lock()
return c, sync.OnceFunc(func() {
p.locks[idx].Unlock()
})
}
// getCheckerNonExclusive returns the first checker without locking.
func (p *checkerPool) getCheckerNonExclusive() (*checker.Checker, func()) {
p.createCheckers()
return p.checkers[0], noop
}
func (p *checkerPool) createCheckers() {
p.createCheckersOnce.Do(func() {
checkerCount := len(p.checkers)
wg := core.NewWorkGroup(p.program.SingleThreaded())
for i := range checkerCount {
wg.Queue(func() {
var tracer *checker.Tracer
if p.tracing != nil {
tracer = checker.NewTracer(p.tracing, i)
}
p.checkers[i], p.locks[i] = checker.NewChecker(p.program, tracer)
})
}
wg.RunAndWait()
p.fileAssociations = make(map[*ast.SourceFile]*checker.Checker, len(p.program.files))
for i, file := range p.program.files {
p.fileAssociations[file] = p.checkers[i%checkerCount]
}
})
}
// Runs `cb` for each checker in the pool concurrently, locking and unlocking checker mutexes as it goes,
// making it safe to call `forEachCheckerParallel` from many threads simultaneously.
func (p *checkerPool) forEachCheckerParallel(cb func(idx int, c *checker.Checker)) {
p.createCheckers()
wg := core.NewWorkGroup(p.program.SingleThreaded())
for idx, checker := range p.checkers {
wg.Queue(func() {
p.locks[idx].Lock()
defer p.locks[idx].Unlock()
cb(idx, checker)
})
}
wg.RunAndWait()
}
func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic {
p.createCheckers()
globalDiagnostics := make([][]*ast.Diagnostic, len(p.checkers))
p.forEachCheckerParallel(func(idx int, checker *checker.Checker) {
globalDiagnostics[idx] = checker.GetGlobalDiagnostics()
})
return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...))
}
// forEachCheckerGroupDo runs one task per checker in parallel. Each task iterates
// the provided files, processing only those assigned to its checker. Within each
// checker's set, files are visited in their original order.
func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile)) {
p.createCheckers()
checkerCount := len(p.checkers)
wg := core.NewWorkGroup(singleThreaded)
for checkerIdx := range checkerCount {
wg.Queue(func() {
p.locks[checkerIdx].Lock()
defer p.locks[checkerIdx].Unlock()
for i, file := range files {
if checker := p.checkers[checkerIdx]; checker == p.fileAssociations[file] {
cb(checker, i, file)
}
}
})
}
wg.RunAndWait()
}
func noop() {}