Skip to content

Commit 048a193

Browse files
authored
plan(v2.15): SPEC-UTIL-004/005/006 backlog — Utility Performance optimizations (#707)
v2.14.0 Utility Hardening 리뷰(manager-quality multi-perspective)에서 식별된 3건의 performance 후보를 v2.15 SPEC draft로 문서화. 각 SPEC은 non-breaking 보장, P2 Medium priority, v2.14.0 머지 완료를 전제. ## SPECs ### SPEC-UTIL-004 — ast-grep ScanMultiple Goroutine Spawn Pattern Alignment - IMP-V3U-022 - 목표: 세마포어 획득을 goroutine 외부로 이동 (MX validator validator.go:513 패턴 정렬) - 파일: internal/hook/security/ast_grep.go - 4 REQs / 3 ACs - 의존: SPEC-UTIL-002 (이미 merge 예정) ### SPEC-UTIL-005 — walkSourceFiles Incremental Scan - IMP-V3U-023 - 목표: post-tool-use 훅의 full-tree 재귀 탐색을 changed-files only incremental로 전환 (monorepo 90% latency 감소 타겟) - 파일: internal/hook/quality/astgrep_gate.go - 6 REQs / 4 ACs - 의존: SPEC-UTIL-002 (이미 merge 예정) ### SPEC-UTIL-006 — analyzeFile Tree-sitter Parse Cache - IMP-V3U-024 - 목표: 파일당 tree-sitter 파싱 1회로 축소 (함수별 반복 제거, 80% 시간 감소 타겟) - 파일: internal/hook/mx/validator.go + internal/hook/mx/complexity/ (ParseFile + MeasureWithTree API 추가) - 6 REQs / 4 ACs - 의존: SPEC-UTIL-001 (이미 merge 예정) ## Suggested Implementation Order (risk × value) 1. SPEC-UTIL-004 — 최저 위험, 가장 빠른 win (세마포어 위치 변경) 2. SPEC-UTIL-005 — 중간 위험, 최고 사용자 가시성 3. SPEC-UTIL-006 — 최고 위험, 최대 코드 영역 (tree-sitter 메모리 lifecycle 주의) ## Cross-SPEC Dependencies - UTIL-004 ↔ UTIL-001 (패턴 레퍼런스, not blocking) - UTIL-005 ↔ UTIL-002 (exclusion 규칙 재사용, blocking) - UTIL-006 ↔ UTIL-001 (complexity 패키지 확장, blocking) - 모두 ↔ v2.14.0 main 머지 완료 (prerequisite) ## /moai run 시 해결 과제 - UTIL-005: internal/hook/payload.go의 ChangedFiles 필드 존재 여부 RED phase에서 검증 (없으면 Option B git diff fallback으로 scope 확장) - UTIL-006: BenchmarkAnalyzeFile_BeforeCache baseline 측정 후 80% 감소 AC 확정 - UTIL-006: research.md 135 LOC → annotation cycle에서 trim 가능 (API design sketch 간소화) ## Source - v2.14.0 code review (manager-quality) Warnings 3건: ast_grep.go:207-215, astgrep_gate.go:29-41, validator.go:196-223 - PR #703 body의 "Deferred (v2.15)" 섹션과 일치 🗿 MoAI <email@mo.ai.kr>
1 parent c9f7cba commit 048a193

6 files changed

Lines changed: 670 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# SPEC-UTIL-004 Research Notes
2+
3+
## §1. 배경 (v2.14 Review Finding)
4+
5+
v2.14.0 릴리스 플랜 Phase 3.2 완료 후 `manager-quality` 멀티관점 코드 리뷰에서 다음 항목이 **Warning — PERFORMANCE** 등급으로 제기되었다.
6+
7+
- **Location**: `internal/hook/security/ast_grep.go:207-215`
8+
- **Category**: 성능 — 메모리 압박
9+
- **Severity**: Warning (v2.14 blocking은 아니며 v2.15 backlog로 defer)
10+
- **Reviewer Note**: "세마포어 획득이 goroutine 내부에서 일어남 → N개 파일이면 N개 goroutine 즉시 생성, 세마포어는 `NumCPU()*2`만 동시 실행. 대용량 파일셋(수천 개)에서 goroutine object 메모리 압박."
11+
- **Reference Pattern**: MX validator(`internal/hook/mx/validator.go:513`)는 `sem <- struct{}{}` 획득이 `go func()` 호출 **이전**에 실행되어 동시 goroutine 수가 세마포어 용량과 동일.
12+
13+
## §2. Baseline (현재 동작)
14+
15+
### 2.1 현재 ScanMultiple 패턴 (simplified)
16+
17+
```
18+
for _, file := range files {
19+
wg.Add(1)
20+
go func(f string) {
21+
sem <- struct{}{}
22+
defer func() { <-sem }()
23+
defer wg.Done()
24+
res, err := Scan(ctx, f, rules)
25+
// ...
26+
}(file)
27+
}
28+
wg.Wait()
29+
```
30+
31+
### 2.2 MX validator 차이 (post-SPEC-UTIL-001)
32+
33+
```
34+
for _, file := range files {
35+
sem <- struct{}{} // outside go func
36+
wg.Add(1)
37+
go func(f string) {
38+
defer func() { <-sem }()
39+
defer wg.Done()
40+
analyzeFile(f, ...)
41+
}(file)
42+
}
43+
wg.Wait()
44+
```
45+
46+
### 2.3 메모리 영향 추정
47+
48+
- Go goroutine 초기 stack: ~2KB (runtime/runtime2.go `_StackMin`)
49+
- 10,000 파일 × 2KB = ~20MB 추가 heap pressure (GC 스캔 대상)
50+
- 세마포어 블록 시 goroutine은 gopark → M 반환하지만 goroutine object는 heap에 유지
51+
- Windows post-tool-use 훅에서 10k+ 파일 프로젝트의 저장 빈도가 높을 경우 누적 부담
52+
53+
### 2.4 AC-UTIL-002-07 검증 gap
54+
55+
SPEC-UTIL-002의 `TestScanMultiple_SemaphoreBound` 테스트는 `Scan` 내부에서 atomic counter를 increment/decrement하여 **동시 실행 중인 `Scan` 호출 수**의 상한을 확인한다. 그러나 실제 **생성된 goroutine 수**(세마포어에서 블록 중인 것 포함)는 측정하지 않으므로, pre-fix 패턴도 이 테스트를 통과한다.
56+
57+
## §3. Approach
58+
59+
세마포어 획득 위치를 `go func()` 호출 **외부로** 이동한다. 이렇게 하면 for 루프(생산자)가 세마포어 슬롯 확보 시까지 블록되어, 동시 생성 goroutine 수가 세마포어 용량(`NumCPU()*2`)으로 자동 제한된다. `defer <-sem`은 goroutine 내부에 유지되어 `Scan` 완료 후 해제 타이밍은 변경되지 않는다.
60+
61+
### 3.1 변경 Diff 개요
62+
63+
```
64+
for _, file := range files {
65+
+ sem <- struct{}{}
66+
wg.Add(1)
67+
go func(f string) {
68+
- sem <- struct{}{}
69+
defer func() { <-sem }()
70+
defer wg.Done()
71+
// ...
72+
}(file)
73+
}
74+
```
75+
76+
### 3.2 검증 전략
77+
78+
- **Peak goroutine counter shim**: `ScanMultiple` 진입 전 `runtime.NumGoroutine()` baseline 기록, atomic counter로 스폰/종료 카운트, 1000-file fixture로 peak 관찰
79+
- **Backward-compat smoke**: 기존 `TestScanMultiple_SemaphoreBound` 회귀 없음 확인
80+
- **Benchmark regression guard**: `BenchmarkScanMultiple_1kFiles` 처리량 ±5% 이내 유지
81+
82+
## §4. References
83+
84+
- v2.14 `manager-quality` 멀티관점 리뷰 보고서 (Warning — PERFORMANCE 섹션)
85+
- SPEC-UTIL-002 §3 (ast-grep 통합 스코프)
86+
- SPEC-UTIL-001 §4.2 (MX validator worker pool 세마포어 계약, `validator.go:513`)
87+
- Go runtime `sync.WaitGroup` + buffered channel semaphore idiom (stdlib test 예시 `runtime/pprof/pprof_test.go`)

.moai/specs/SPEC-UTIL-004/spec.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
id: SPEC-UTIL-004
3+
title: "ast-grep ScanMultiple Goroutine Spawn Pattern Alignment"
4+
version: "0.1.0"
5+
status: draft
6+
created: 2026-04-24
7+
updated: 2026-04-24
8+
author: MoAI v2.15 Backlog Writer
9+
priority: P2 Medium
10+
phase: "v2.15 — Utility Performance Backlog"
11+
module: "internal/hook/security/"
12+
dependencies: []
13+
related_problem: [IMP-V3U-022]
14+
related_pattern: []
15+
related_principle: []
16+
related_decision: []
17+
related_theme: "v2.15 Utility Performance"
18+
breaking: false
19+
bc_id: []
20+
lifecycle: spec-anchored
21+
tags: "astgrep, goroutine, semaphore, performance"
22+
---
23+
24+
# SPEC-UTIL-004: ast-grep ScanMultiple Goroutine Spawn Pattern Alignment
25+
26+
## HISTORY
27+
28+
| Version | Date | Author | Description |
29+
|---------|------|--------|-------------|
30+
| 0.1.0 | 2026-04-24 | MoAI v2.15 Backlog Writer | Initial draft. v2.14 `manager-quality` 멀티관점 코드 리뷰에서 발견된 Warning 등급 성능 이슈(PERFORMANCE, `internal/hook/security/ast_grep.go:207-215`). SPEC-UTIL-002 머지 이후 착수 가능. Non-breaking, public API surface unchanged. |
31+
32+
---
33+
34+
## 1. Goal (목적)
35+
36+
`ast_grep.go` `ScanMultiple`의 goroutine 스폰 패턴을 MX validator(`internal/hook/mx/validator.go:513`) 패턴에 정렬하여, 대용량 파일셋(수천 개)에서 N개 goroutine이 즉시 생성되던 기존 동작을 동시 실행 상한(`runtime.NumCPU()*2`)과 동일한 수의 goroutine 수로 제한한다. 세마포어 획득 위치를 `go func()` 내부에서 외부로 이동하여 goroutine object 메모리 압박을 제거하며, 기존 세마포어 실행 상한 보증은 그대로 유지된다.
37+
38+
## 2. Scope (범위)
39+
40+
### 2.1 In Scope
41+
42+
- `internal/hook/security/ast_grep.go``ScanMultiple` 스폰 루프 변경 (라인 207-215 범위)
43+
- 세마포어 `sem <- struct{}{}` 획득을 `go func()` 호출 **이전**으로 이동
44+
- `defer <-sem`은 goroutine 내부 유지 (기존 패턴 그대로)
45+
- 1000-file synthetic fixture 기반 peak goroutine count 측정 테스트 추가
46+
47+
### 2.2 Out of Scope
48+
49+
- Public API 표면 변경 금지 (`ScanMultiple` 시그니처, 반환 타입, 에러 계약 불변)
50+
- `ast_grep.go`의 다른 함수(`Scan`, `scanFile` 등) 수정 금지
51+
- 세마포어 용량(`runtime.NumCPU()*2`) 변경 금지 — 실행 상한 정책은 유지
52+
- 워커 풀 재설계(채널 기반 worker pool 도입 등) 금지 — v3.0 breaking 이슈
53+
54+
## 3. Environment (환경)
55+
56+
### 3.1 현재 코드 상태
57+
58+
`ast_grep.go:207-215` (pre-fix):
59+
60+
```
61+
for _, file := range files {
62+
wg.Add(1)
63+
go func(f string) {
64+
sem <- struct{}{} // ← 세마포어 획득이 goroutine 내부
65+
defer func() { <-sem }()
66+
defer wg.Done()
67+
// ... Scan 호출 ...
68+
}(file)
69+
}
70+
```
71+
72+
- N개 파일 → N개 goroutine 즉시 생성
73+
- 세마포어는 `NumCPU()*2`만 동시 `Scan` 실행 허용
74+
- 나머지 goroutine은 세마포어 채널에서 블록 대기 (goroutine object 메모리 할당됨)
75+
- 10k 파일 × ~2KB goroutine stack → 약 20MB 피크 메모리 압박
76+
77+
### 3.2 MX validator 대비 (reference pattern)
78+
79+
`internal/hook/mx/validator.go:513` (post-SPEC-UTIL-001, 현재 main):
80+
81+
```
82+
for _, file := range files {
83+
sem <- struct{}{} // ← 세마포어 획득이 go 호출 이전
84+
wg.Add(1)
85+
go func(f string) {
86+
defer func() { <-sem }()
87+
defer wg.Done()
88+
// ... analyzeFile 호출 ...
89+
}(file)
90+
}
91+
```
92+
93+
- 동시 goroutine 수 = 세마포어 용량 = `NumCPU()*2`
94+
- 생산자(for 루프)가 세마포어에서 블록되어 goroutine 생성 속도 조절
95+
96+
### 3.3 v2.14 AC-UTIL-002-07 검증 gap
97+
98+
SPEC-UTIL-002의 `AC-UTIL-002-07` (TestScanMultiple_SemaphoreBound)는 **활성 `Scan` 호출 수**만 검증하며, 실제 **생성된 goroutine 수**는 검증하지 않는다. 따라서 pre-fix 패턴도 기존 테스트를 통과한다.
99+
100+
## 4. Requirements (요구사항, EARS)
101+
102+
- **REQ-UTIL-004-001** (Ubiquitous): `ScanMultiple`은 세마포어 획득(`sem <- struct{}{}`)을 `go func(...)` 호출 **이전**에 실행해야 한다.
103+
- **REQ-UTIL-004-002** (Ubiquitous): `defer <-sem`은 goroutine 내부에 유지해야 한다. 해제 타이밍은 기존 계약(`Scan` 호출 완료 후)과 동일하게 보전된다.
104+
- **REQ-UTIL-004-003** (Event-Driven): 1000-file synthetic fixture를 `ScanMultiple`에 주입했을 때, atomic counter shim이 관찰한 peak goroutine count는 `runtime.NumCPU()*2` 이하여야 한다 (예: `NumCPU=4` 환경에서 peak ≤ 8).
105+
- **REQ-UTIL-004-004** (Ubiquitous): 변경은 non-breaking이다. `ScanMultiple`의 시그니처, 반환 타입, 에러 계약, 공개 식별자 surface는 byte-identical이어야 한다.
106+
107+
## 5. Acceptance Criteria (수락 기준)
108+
109+
- **AC-UTIL-004-01**: 1000-file synthetic fixture + atomic counter shim 조합으로 `TestScanMultiple_PeakGoroutineBound` 추가. `NumCPU=4` 환경에서 peak goroutine count ≤ 8 관찰.
110+
- **AC-UTIL-004-02**: 기존 `TestScanMultiple_SemaphoreBound` (v2.14 AC-UTIL-002-07) 통과 유지. 활성 `Scan` 호출 수 상한 검증은 회귀 없음.
111+
- **AC-UTIL-004-03**: apigen snapshot 도구로 `internal/hook/security/` 공개 식별자 surface가 pre-fix와 byte-identical임을 확인.
112+
113+
## 6. Constraints (제약)
114+
115+
- Non-breaking: API 시그니처, 에러 타입, JSON 출력 스키마 전부 불변
116+
- stdlib only: `sync`, `runtime`, `sync/atomic` 사용. 새로운 외부 의존성 금지
117+
- SPEC-UTIL-002 머지 완료 후 착수 (현재 `release/v2.14.0` 브랜치에서 머지 대기)
118+
- Windows/macOS/Linux 3개 플랫폼 CI 모두 통과해야 함
119+
120+
## 7. Risks (위험)
121+
122+
- **R1 (저위험)**: 세마포어 획득 위치 변경으로 `ScanMultiple` 진입 지연 가능 (생산자가 세마포어 대기). Mitigation: 기존 MX validator에서 이미 검증된 패턴이며 동일 코드에서 사용 중. 성능 회귀 없음을 `BenchmarkScanMultiple_1kFiles`로 확인.
123+
124+
## 8. Dependencies (의존성)
125+
126+
- **Blocked by**: 없음 (SPEC-UTIL-002는 `release/v2.14.0`에서 머지 대기, v2.15 착수 시점에는 main 반영됨)
127+
- **Blocks**: 없음
128+
- **Related**: SPEC-UTIL-002 (ast-grep 통합 원본), SPEC-UTIL-001 §4.2 (MX validator 세마포어 패턴 reference)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SPEC-UTIL-005 Research Notes
2+
3+
## §1. 배경 (v2.14 Review Finding)
4+
5+
v2.14.0 Phase 3.2 완료 후 `manager-quality` 멀티관점 코드 리뷰에서 다음 항목이 **Warning — PERFORMANCE** 등급으로 제기되었다.
6+
7+
- **Location**: `internal/hook/quality/astgrep_gate.go:29-41` (walkSourceFiles), 호출부 `RunAstGrepGateV2`
8+
- **Category**: 성능 — 불필요한 I/O 및 CPU 반복
9+
- **Severity**: Warning (v2.14 blocking은 아니며 v2.15 backlog로 defer)
10+
- **Reviewer Note**: "`walkSourceFiles(projectDir)``RunAstGrepGateV2` 호출마다 프로젝트 전체 디렉토리 트리를 재귀 탐색. Post-tool-use 훅은 파일 저장 시마다 트리거되므로 대형 모노레포(~10k 파일)에서 매 저장마다 전체 스캔 비용 누적."
11+
- **Approach Hint**: "git diff 기반 changed-files only 또는 결과 캐싱. hook payload에 changedFiles 제공 시 활용."
12+
13+
## §2. Baseline (현재 비용 구조)
14+
15+
### 2.1 현재 동작
16+
17+
- `filepath.WalkDir(projectDir, ...)` 전체 트리 재귀
18+
- 각 entry마다 exclusion check (vendor, node_modules, .git, ... — SPEC-UTIL-002 §3 규칙)
19+
- source-file 필터(확장자 기반) + suppression pairing 수집
20+
21+
### 2.2 경험적 비용 (측정 필요, 현 시점 가정치)
22+
23+
| 파일 수 | 추정 소요시간 (SSD, macOS) |
24+
|---------|-----------------------|
25+
| 500 | ~2ms |
26+
| 5,000 | ~20ms |
27+
| 10,000 | ~40-80ms |
28+
| 50,000 (대형 monorepo) | ~200-500ms |
29+
30+
- Post-tool-use 훅이 활발한 개발 세션(분당 5-10회 저장)에서 10k-file 기준 누적 ~200-800ms/분
31+
- 실제 변경된 파일은 통상 1-5개 → 99%의 파일은 매번 무의미하게 스캔됨
32+
33+
### 2.3 Hook payload 구조 확인 포인트
34+
35+
`internal/hook/payload.go`(혹은 유사 파일)에 `PostToolUsePayload { ChangedFiles []string; ... }` 형태로 필드가 존재하는지 Plan 단계에서 재확인 필요. 존재하지 않는 경우 Options 섹션의 Option B로 fallback.
36+
37+
## §3. Options
38+
39+
### Option A — hook payload의 changedFiles 활용 (**preferred**)
40+
41+
- hook runtime이 이미 변경 파일 리스트를 알고 있음 → `RunAstGrepGateV2(ctx, projectDir, changedFiles)` 형태로 전달
42+
- full scan 대비 99% I/O 제거
43+
- Risk: payload의 정확성에 의존. Claude Code 측에서 누락 발생 시 탐지 불가 → `changedFiles == nil` 가드로 폴백
44+
45+
### Option B — git diff --name-only HEAD (fallback)
46+
47+
- `git diff --name-only HEAD` + `git ls-files --others --exclude-standard` 조합으로 changed + untracked 산출
48+
- 장점: hook payload 독립 — 어떤 훅 트리거에서도 동작
49+
- 단점: 외부 프로세스 exec 비용 (~10-30ms), stash/partial index 상태 처리 복잡
50+
- Windows/Linux git 경로 normalization 추가 필요
51+
52+
### Option C — 파일 mtime 기반 결과 캐싱 (**rejected**)
53+
54+
- staleness 위험: 외부 에디터/포매터가 파일 수정 시 캐시 무효화 신호 없음
55+
- 동일 파일이 여러 훅 호출 간 변경되어도 mtime 해상도 한계(초 단위)로 놓칠 가능성
56+
57+
### 3.1 결정: Option A + Option B 복합
58+
59+
- Primary: hook payload의 changedFiles 사용
60+
- Secondary fallback: payload nil/empty 시 기존 full scan 유지 (backward-compat)
61+
- Git diff는 v3.0 이슈로 defer (현재 SPEC 범위 밖)
62+
63+
## §4. References
64+
65+
- v2.14 `manager-quality` 멀티관점 리뷰 보고서 (Warning — PERFORMANCE 섹션)
66+
- SPEC-UTIL-002 §3 (walkSourceFiles 원본 + exclusion 규칙)
67+
- SPEC-UTIL-003 (Phase 3.3 quality gate 통합 테스트 — incremental path 회귀 가드 재사용 가능)
68+
- `internal/hook/payload.go` 혹은 대응 훅 paylaod 정의 (Plan 단계에서 재확인)

0 commit comments

Comments
 (0)