2025-10-15 10:12:44 +03:00

237 lines
5.8 KiB
Go

package project
import (
"context"
"fmt"
"iter"
"sync"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
)
type checkerPool struct {
maxCheckers int
program *compiler.Program
mu sync.Mutex
cond *sync.Cond
createCheckersOnce sync.Once
checkers []*checker.Checker
inUse map[*checker.Checker]bool
fileAssociations map[*ast.SourceFile]int
requestAssociations map[string]int
log func(msg string)
}
var _ compiler.CheckerPool = (*checkerPool)(nil)
func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg string)) *checkerPool {
pool := &checkerPool{
program: program,
maxCheckers: maxCheckers,
checkers: make([]*checker.Checker, maxCheckers),
inUse: make(map[*checker.Checker]bool),
requestAssociations: make(map[string]int),
log: log,
}
pool.cond = sync.NewCond(&pool.mu)
return pool
}
func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
p.mu.Lock()
defer p.mu.Unlock()
requestID := core.GetRequestID(ctx)
if requestID != "" {
if checker, release := p.getRequestCheckerLocked(requestID); checker != nil {
return checker, release
}
}
if p.fileAssociations == nil {
p.fileAssociations = make(map[*ast.SourceFile]int)
}
if index, ok := p.fileAssociations[file]; ok {
checker := p.checkers[index]
if checker != nil {
if inUse := p.inUse[checker]; !inUse {
p.inUse[checker] = true
if requestID != "" {
p.requestAssociations[requestID] = index
}
return checker, p.createRelease(requestID, index, checker)
}
}
}
checker, index := p.getCheckerLocked(requestID)
p.fileAssociations[file] = index
return checker, p.createRelease(requestID, index, checker)
}
func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) {
p.mu.Lock()
defer p.mu.Unlock()
checker, index := p.getCheckerLocked(core.GetRequestID(ctx))
return checker, p.createRelease(core.GetRequestID(ctx), index, checker)
}
func (p *checkerPool) Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] {
panic("unimplemented")
}
func (p *checkerPool) GetAllCheckers(ctx context.Context) ([]*checker.Checker, func()) {
p.mu.Lock()
defer p.mu.Unlock()
requestID := core.GetRequestID(ctx)
if requestID == "" {
panic("cannot call GetAllCheckers on a project.checkerPool without a request ID")
}
// A request can only access one checker
if c, release := p.getRequestCheckerLocked(requestID); c != nil {
return []*checker.Checker{c}, release
}
c, release := p.GetChecker(ctx)
return []*checker.Checker{c}, release
}
func (p *checkerPool) getCheckerLocked(requestID string) (*checker.Checker, int) {
if checker, index := p.getImmediatelyAvailableChecker(); checker != nil {
p.inUse[checker] = true
if requestID != "" {
p.requestAssociations[requestID] = index
}
return checker, index
}
if !p.isFullLocked() {
checker, index := p.createCheckerLocked()
p.inUse[checker] = true
if requestID != "" {
p.requestAssociations[requestID] = index
}
return checker, index
}
checker, index := p.waitForAvailableChecker()
p.inUse[checker] = true
if requestID != "" {
p.requestAssociations[requestID] = index
}
return checker, index
}
func (p *checkerPool) getRequestCheckerLocked(requestID string) (*checker.Checker, func()) {
if index, ok := p.requestAssociations[requestID]; ok {
checker := p.checkers[index]
if checker != nil {
if inUse := p.inUse[checker]; !inUse {
p.inUse[checker] = true
return checker, p.createRelease(requestID, index, checker)
}
// Checker is in use, but by the same request - assume it's the
// same goroutine or is managing its own synchronization
return checker, noop
}
}
return nil, noop
}
func (p *checkerPool) getImmediatelyAvailableChecker() (*checker.Checker, int) {
for i, checker := range p.checkers {
if checker == nil {
continue
}
if inUse := p.inUse[checker]; !inUse {
return checker, i
}
}
return nil, -1
}
func (p *checkerPool) waitForAvailableChecker() (*checker.Checker, int) {
p.log("checkerpool: Waiting for an available checker")
for {
p.cond.Wait()
checker, index := p.getImmediatelyAvailableChecker()
if checker != nil {
return checker, index
}
}
}
func (p *checkerPool) createRelease(requestId string, index int, checker *checker.Checker) func() {
return func() {
p.mu.Lock()
defer p.mu.Unlock()
delete(p.requestAssociations, requestId)
if checker.WasCanceled() {
// Canceled checkers must be disposed
p.log(fmt.Sprintf("checkerpool: Checker for request %s was canceled, disposing it", requestId))
p.checkers[index] = nil
delete(p.inUse, checker)
} else {
p.inUse[checker] = false
}
p.cond.Signal()
}
}
func (p *checkerPool) isFullLocked() bool {
for _, checker := range p.checkers {
if checker == nil {
return false
}
}
return true
}
func (p *checkerPool) createCheckerLocked() (*checker.Checker, int) {
for i, existing := range p.checkers {
if existing == nil {
checker := checker.NewChecker(p.program)
p.checkers[i] = checker
return checker, i
}
}
panic("called createCheckerLocked when pool is full")
}
func (p *checkerPool) isRequestCheckerInUse(requestID string) bool {
p.mu.Lock()
defer p.mu.Unlock()
if index, ok := p.requestAssociations[requestID]; ok {
checker := p.checkers[index]
if checker != nil {
return p.inUse[checker]
}
}
return false
}
func (p *checkerPool) size() int {
p.mu.Lock()
defer p.mu.Unlock()
size := 0
for _, checker := range p.checkers {
if checker != nil {
size++
}
}
return size
}
func noop() {}