Skip to content

Commit 8f81c77

Browse files
committed
feat: Concurrency
1 parent 488a71c commit 8f81c77

2 files changed

Lines changed: 70 additions & 6 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# lx: Controllable AI for Gophers!
1+
# lx: Controllable AI for Gophers
22
`lx` restores the [joy of wrangling code](https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes) by letting you define the forest, while AI plants the trees under your intentional command.
33

44
---
@@ -52,6 +52,12 @@ You drive the flow and the contract; `lx` uses runtime traces to fill in the fun
5252
Code for any target, from any host.
5353
With Build Tags support, lx bridges the gap between your development machine and your production environment.
5454

55+
## Concurrency
56+
Time is the only non-renewable resource for developers.
57+
`lx` doesn't process functions sequentially.
58+
It utilizes an Asynchronous Worker Pool to generate code for multiple functions in parallel.
59+
Whether you have 5 functions or 50, the total execution time converges to the duration of the single slowest request, not the sum of them all.
60+
5561
---
5662

5763
# Quick Start
@@ -223,7 +229,7 @@ lx -tags lx_mock .
223229
1. Injects spies into target functions (wraps returns with `lx.Spy(...)`)
224230
2. Runs `go run .` inside the target directory to capture real inputs and expected outputs
225231
3. Reverts the injected spies immediately (restores your source files)
226-
4. Generates and patches the function bodies based on the captured evidence
232+
4. Generates function bodies in parallel (Async) and patches them based on the captured evidence
227233

228234
## Useful flags (optional)
229235

cmd/main.go

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path/filepath"
2424
"regexp"
2525
"strings"
26+
"sync"
2627
"syscall"
2728
"time"
2829

@@ -75,7 +76,11 @@ type commandLLM struct {
7576
args []string // <-- [수정 2] 템플릿 저장용 필드 추가
7677
}
7778

79+
var logMu sync.Mutex
80+
7881
func main() {
82+
startTime := time.Now()
83+
7984
var (
8085
showVersion bool
8186
opts options
@@ -150,11 +155,42 @@ func main() {
150155
return
151156
}
152157

158+
// [수정] 비동기 실행을 위한 설정
159+
var wg sync.WaitGroup
160+
// 최대 동시 실행 수 제한 (예: 5개). 너무 많이 띄우면 API Rate Limit에 걸립니다.
161+
semaphore := make(chan struct{}, 5)
162+
163+
// 파일별 쓰기 충돌 방지를 위한 Mutex 맵
164+
fileLocks := make(map[string]*sync.Mutex)
165+
for _, t := range targets {
166+
if _, exists := fileLocks[t.FilePath]; !exists {
167+
fileLocks[t.FilePath] = &sync.Mutex{}
168+
}
169+
}
170+
153171
for _, target := range targets {
154-
processSingleTarget(opts, llm, cfg, target)
172+
wg.Add(1)
173+
// 고루틴 실행
174+
go func(t TargetInfo) {
175+
defer wg.Done()
176+
177+
// 세마포어 획득 (슬롯이 비을 때까지 대기)
178+
semaphore <- struct{}{}
179+
defer func() { <-semaphore }() // 작업 완료 후 반납
180+
181+
// 해당 파일 전용 락 가져오기
182+
fileMu := fileLocks[t.FilePath]
183+
184+
// 비동기 처리 함수 호출
185+
processSingleTarget(opts, llm, cfg, t, fileMu)
186+
}(target)
155187
}
156188

157-
fmt.Println("[lx] All tasks completed")
189+
// 모든 작업이 끝날 때까지 대기
190+
wg.Wait()
191+
192+
elapsed := time.Since(startTime)
193+
fmt.Printf("[lx] All tasks completed in %s\n", elapsed)
158194
}
159195

160196
func setupSafeExit(backups map[string]fileBackup) {
@@ -697,15 +733,24 @@ func scanProjectForLx(root string) []TargetInfo {
697733
return targets
698734
}
699735

700-
func processSingleTarget(opts options, llm LLM, cfg *Config, target TargetInfo) {
736+
func processSingleTarget(opts options, llm LLM, cfg *Config, target TargetInfo, fileMu *sync.Mutex) {
701737
displayPath := target.FilePath
702738
taskName := fmt.Sprintf("[%s -> %s]", displayPath, target.FuncName)
739+
740+
logMu.Lock()
703741
fmt.Printf("[lx] %s Generate code\n", taskName)
742+
logMu.Unlock()
743+
744+
fileMu.Lock()
704745

705746
fset := token.NewFileSet()
747+
706748
node, err := parser.ParseFile(fset, target.FilePath, nil, parser.ParseComments)
707749
if err != nil {
750+
fileMu.Unlock()
751+
logMu.Lock()
708752
fmt.Printf("[lx] %s parse failed: %v\n", taskName, err)
753+
logMu.Unlock()
709754
return
710755
}
711756

@@ -717,14 +762,20 @@ func processSingleTarget(opts options, llm LLM, cfg *Config, target TargetInfo)
717762
}
718763
return true
719764
})
765+
720766
if currentFn == nil || currentFn.Body == nil {
767+
fileMu.Unlock()
768+
logMu.Lock()
721769
fmt.Printf("[lx] %s function not found or has no body\n", taskName)
770+
logMu.Unlock()
722771
return
723772
}
724773

725774
signature := extractSignature(fset, currentFn)
726-
prompt := truncateString(singleLine(target.Prompt), opts.maxPromptChars)
727775

776+
fileMu.Unlock()
777+
778+
prompt := truncateString(singleLine(target.Prompt), opts.maxPromptChars)
728779
isVoid := currentFn.Type.Results == nil || len(currentFn.Type.Results.List) == 0
729780

730781
outputSection := ""
@@ -772,19 +823,26 @@ RULES:
772823

773824
generatedCode, err := llm.Generate(ctx, cfg.Model, systemPrompt)
774825
if err != nil {
826+
logMu.Lock()
775827
fmt.Printf("[lx] %s code generation failed\n", taskName)
776828
fmt.Printf("[lx] Error: %s\n", diagnoseLLMError(err))
829+
logMu.Unlock()
777830
return
778831
}
779832

780833
cleaned := cleanAICode(generatedCode)
781834
deps := extractDependencies(cleaned)
782835

836+
fileMu.Lock()
837+
defer fileMu.Unlock()
838+
783839
if ok := applyCodeToFile(target.FilePath, currentFn, fset, prompt, cleaned); ok {
840+
logMu.Lock()
784841
fmt.Printf("[lx] %s complete\n", taskName)
785842
if len(deps) > 0 {
786843
fmt.Printf("[lx] %s deps (manual): %s\n", taskName, strings.Join(uniqueStrings(deps), ", "))
787844
}
845+
logMu.Unlock()
788846
}
789847
}
790848

0 commit comments

Comments
 (0)