Skip to content

fix(perf): boundary perf 예산을 동일 작업량 baseline 대비 비율로 교체 (CI flake) - #44

Merged
hsleedevelop merged 7 commits into
mainfrom
ao/harness-aijient-team-plugin-22/boundary-perf-invariant
Aug 25, 2026
Merged

hsleedevelop merged 7 commits into
mainfrom
ao/harness-aijient-team-plugin-22/boundary-perf-invariant

Conversation

@hsleedevelop

@hsleedevelop hsleedevelop commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

문제

tests/perf/boundary-checkpoint.test.mjs의 boundary performance가 GitHub Actions에서 무작위로 실패했다.
문서 전용 diff에서도 발생 → 변경 내용과 무관한 측정 방식의 결함.

깨지던 assertion은 상대 예산 coldMs - spawnFloorMs < 75. 분모가 bare node -e '' 스폰이라
스폰 비용 변동만 상쇄한다. 부하에서는 CLI 본체 작업(20 × 10KiB read+parse + ~20개 모듈 그래프)도
함께 느려지는데 그 작업량 비례 지연을 분모가 흡수하지 못한다.

max-parallel: 1 은 no-op — 측정으로 기각

원 가설은 "matrix 두 job 동시 실행 경합"이었으나 사실이 아니다. 러너 메타데이터로 확인
(로그 blob은 프록시 403이지만 JSON API는 열려 있다):

$ gh api repos/bd-makers/team-harness/actions/runs/32802292903/jobs
test (20)  runner_name="GitHub Actions 1000000304"  failure
test (18)  runner_name="GitHub Actions 1000000305"  success
runner_group_name="GitHub Actions" (hosted)

두 job은 서로 다른 hosted VM에서 돈다 — 하드웨어를 공유하지 않으므로 직렬화는 무의미하다.
실패 job이 18 ↔ 20을 번갈아 오간 것도 VM별 독립 노이즈에 부합한다.
진짜 부하원은 hosted 러너(2 vCPU) 물리 호스트의 noisy neighbour이며 제거 불가다.
→ 측정을 부하 불변으로 만드는 수밖에 없다. 워크플로우는 건드리지 않았다.

수정: 차(ms) → 비(배수)

CPU starvation 하에서 wall time ≈ 작업량 × 감속계수 s:

형태 결과
차 (W_cold − W_base)·s s가 그대로 남는다
비 (W_cold·s)/(W_base·s) s가 소거된다

분모는 CLI가 읽는 것과 같은 21개 파일을 같은 방식으로(stat→readFile utf8→JSON.parse,
양쪽 순차·boundary 동시) 읽는 baseline이며 src/를 import 하지 않는다.

부수적으로 모든 스폰 shape에 untimed warmup을 추가했다. 부하 중 첫 cold 샘플이 453–541ms로 튀어
(정상 60–90ms) 500ms 절대 상한 자체가 flake 원인이었다(541.3ms 관측). bare 스폰에 이미
적용돼 있던 논리를 대칭 적용한 것이다.

절대 상한 500/800ms는 값 그대로 유지했다. src/ 변경 없음 — 테스트 파일 1개.

증거 (동일 부하·동일 반복 횟수)

수정 전 수정 후
부하 16 (12코어) × 10 6 PASS / 4 FAIL 10 / 0
부하 24 × 10 6 PASS / 4 FAIL 10 / 0
부하 36 (3배 초과구독) × 8 — 8 / 0

비율은 부하를 3배까지 올려도 더 나빠지지 않았다(cold 1.20–1.96) — 불변성이 실제로 성립한다.

회귀 탐지력은 오히려 강해졌다

runBoundaryCheck에 정확한 CPU burn을 주입한 mutation test (FAIL = 회귀를 잡음):

주입 신규 가드 구 가드
+30ms PASS PASS
+40ms FAIL ✅ PASS ❌
+50ms FAIL ✅ PASS ❌
+60ms FAIL ✅ FAIL ✅

탐지 임계 +58ms → +35ms. "green 될 때까지 경계를 푼" 것이 아니다.

검증

  • npm test (node 20): 415 tests, 0 fail
  • node 18은 이 머신에서 검증하지 못했다 — 설치 차단(프록시), docker 미설치, 로컬 바이너리 없음.
    node 18에 대해 확보한 것은 CI green 1회뿐이고 부하 특성은 미측정이다. 위 경고가 그대로 적용된다.
  • node 24는 통과하지만 checkpoint 비율이 무부하에서도 3.07이다(node 20은 1.68). 원인을 분해했다:
    70ms 전부가 sh + exec 구간이고, 같은 경로를 --version으로 재면 오버헤드가 거의 0 →
    shebang·PATH·sh 스폰 모두 원인이 아니며 파이프 stdin을 sh 경유로 상속받았을 때의 node 24 처리가
    남는 유일한 차이다. (이는 테스트가 아니라 배포되는 hook 자체의 관측이며 후속 후보로 남겼다.)
    CHECKPOINT_BUDGET은 선제적으로 올리지 않았다 — 이상치는 새 버전(24)에서 나타났고
    node 18은 더 오래된 버전이며, 기존 CI 실패 4건은 전부 cold assertion이었다.
    근거 없이 완화하는 것은 이 작업이 피하려던 실패 양식이다.
  • 외부 read-only 리뷰(codex) 실행 · 결과와 판별을 artifact ## Reviews에 기록.
    P2 1건(warmup이 cold-cache 회귀 탐지를 지운다)은 검증 결과 부분 타당 —
    지적한 커버리지는 환경 지배적이었고(그 자체가 flake 발생원), 시작 회귀 탐지는
    node가 매 프로세스마다 모듈 그래프를 재컴파일하므로 사라지지 않는다(실측: warmup 이후
    전 샘플에서 ~9ms 초과분 유지). 실행 가능한 절반(steady-state 명세 분리)만 반영했다.

⚠️ CI green은 필요조건일 뿐 충분조건이 아니다. 관측된 flake 빈도에서 green 1회는 증거가
되지 못한다 — 진짜 증거는 위 부하 재현 수치다.

작업 문서: docs/chad/boundary-perf-invariant/

🤖 Generated with Claude Code

https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx

chad-aijinet and others added 7 commits August 25, 2026 14:15
상대 예산의 분모가 bare `node -e ''` 스폰이라 스폰 비용 변동만 상쇄했다.
부하에서는 CLI 본체 작업(20 x 10KiB read+parse + ~20개 모듈 그래프)도 함께
느려지는데 그 작업량 비례 지연을 분모가 흡수하지 못해 CI에서 무작위로 깨졌다.

예산을 차(ms)에서 비(배수)로 바꾼다. wall time ~ 작업량 x 감속계수이므로
차는 감속계수를 남기지만 비는 소거한다. 분모는 CLI가 읽는 것과 같은 21개
파일을 같은 방식으로 읽고 파싱하되 src/를 import 하지 않는 baseline이다.

matrix 경합설은 기각했다 — `gh api .../jobs` 로 확인한 결과 두 job은 서로
다른 hosted 러너 VM에서 돌아 하드웨어를 공유하지 않으므로 max-parallel:1은
no-op이다. 실제 부하원은 호스트의 noisy neighbour이며 제거 불가다.

모든 스폰 shape에 untimed warmup을 추가한다. 부하 중 첫 cold 샘플이
453-541ms로 튀어(정상 60-90ms) 500ms 절대 상한 자체가 flake 원인이었다.
bare 스폰에 이미 적용돼 있던 논리를 대칭 적용한 것이다.

절대 상한 500/800ms는 값 그대로 유지한다.

증거: 부하 16/24 각 10회에서 수정 전 4회 실패 -> 수정 후 0회 실패,
부하 36(3배 초과구독) 8회도 전부 통과. CPU burn 주입 mutation test에서
신규 가드는 +40ms 회귀를 잡고 구 가드는 놓친다(탐지 임계 +58ms -> +35ms).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
리뷰 P2(warmup이 cold-cache 시작 회귀 탐지를 지운다)를 검증했다.
지적한 커버리지는 실재했으나 환경 지배적이었다 — 첫 cold 샘플이 부하 중
541.3ms로 500ms 상한을 이미 초과해 회귀 탐지기가 아니라 flake 발생원이었다.

시작 회귀 탐지는 사라지지 않았다: node는 매 프로세스마다 모듈 그래프를
다시 컴파일하므로 CLI의 baseline 대비 ~9ms 초과분이 warmup 이후 모든
샘플에서 유지된다. 프로세스 간 캐시가 있었다면 붕괴했어야 할 격차다.

리뷰어 제안 중 실행 가능한 절반(steady-state 명세 분리)만 반영했다 —
테스트 이름과 warmup 주석을 명확히 했다. 동작 변경 없음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
'mise shim 때문'이라는 최초 설명은 측정으로 반증됐다 — 두 버전 모두 실제
바이너리를 쓴다. 비용을 분해한 결과 70ms는 전부 sh+exec 구간이며,
같은 경로를 --version 으로 재면 오버헤드가 거의 0이다. 원인은 파이프 stdin을
sh 를 거쳐 상속받았을 때의 node 24 처리다. 이는 테스트가 아니라 배포되는
hook 자체의 관측이므로 후속 후보로 남긴다.

node 18은 이 머신에서 검증 불가(설치 차단·docker 미설치·로컬 바이너리 없음).
'CI matrix가 커버한다'는 서술이 'green은 충분조건이 아니다'와 모순이라
'green 1회뿐, 부하 특성 미측정'으로 정정했다.

checkpoint 예산은 선제적으로 올리지 않았다 — 이상치는 새 버전(24)에서
나타났고 node 18은 더 오래된 버전이며, 기존 CI 실패 4건은 전부 cold 였다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
이 저장소를 유지보수하는 머신에서는 raw CI 로그를 받을 수 없다 —
*.blob.core.windows.net 이 프록시에서 403이라 gh run view --log-failed 가
실패한다. annotations REST API는 열려 있으므로 실패 메시지를 그쪽으로 흘린다.

perf 진단 라인은 실패 시가 아니라 매 실행마다 남긴다 — 타이밍 flake의
유용한 신호는 통과한 실행의 수치이기 때문이다. node 18 부하 특성이
미측정 상태라 이 값이 곧 그 공백을 메운다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
로컬에서 node 18을 실행할 수 없어 남겨 뒀던 공백을 CI 계측으로 메웠다.
실제 hosted 러너(2 vCPU, Linux) 실측:

  test (18): cold 1.66x · checkpoint 1.74x
  test (20): cold 1.63x · checkpoint 1.74x

예산 3x/5x 대비 여유가 크고 두 버전이 거의 동일하다. checkpoint가 node
18·20 모두 1.74x로 일치하므로, macOS + node 24에서 본 3.07은 그 조합에
국한된 이상치이며 CI로 전이되지 않는다. CHECKPOINT_BUDGET을 선제적으로
올리지 않은 판단이 실측으로 확인됐다.

계측 과정에서 별건 flake를 발견해 기록했다 — node 18에서 간헐적으로 나는
tests/task-templates.test.mjs 의 'Unable to deserialize cloned data' 오류.
이 브랜치가 건드리지 않은 파일이고 동일 코드로 앞선 실행은 통과했으며
재실행으로 통과했다. node:test 러너의 IPC 직렬화 쪽 선재 문제로 별도 추적한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
@hsleedevelop
hsleedevelop merged commit 4b194e0 into main Aug 25, 2026
2 checks passed
hsleedevelop pushed a commit that referenced this pull request Aug 25, 2026
같은 run의 test(20)은 통과했고 perf 수치도 정상(cold 1.70x / checkpoint 1.76x)
이라 #44의 회귀가 아니라 이 task가 다루는 node 18 flake다. annotation 덕에
'exit code 1' 대신 원인이 한 번에 특정됐다.

이 flake가 기본 브랜치를 막는다는 것이 확인됐다 — [24] 이동이 그대로 해소책이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
@hsleedevelop
hsleedevelop deleted the ao/harness-aijient-team-plugin-22/boundary-perf-invariant branch August 25, 2026 09:10
hsleedevelop pushed a commit that referenced this pull request Aug 25, 2026
PR #44 머지(4b194e0) 완료에 따라 boundary-perf-invariant 를 종결하고
원장을 재생성했다. 집계는 AGENTS.md 규약대로 기본 브랜치에서만 갱신한다
(선례: 5e0c731, 03e506a — 둘 다 main 직접 커밋).

  boundary-perf-invariant → ✅ done
  node-test-runner-flake  → 🔄 open (24.20.0 검증 대기)

가드를 두 번 우회했고 사유를 남긴다.

1) done --force — 가드 2건이 재활성화로 인한 오탐이었다. done 은 활성 task
   만 대상이라 끝난 task 를 종결하려면 재활성화해야 하는데, 활성화가
   switchedAt 을 현재 시각으로 갱신하면서 판정 창이 초기화됐다. 실제로는
   리뷰 마커(at=05:35:00Z)도 커밋 6건도 원래 창(활성화 04:25:13Z) 안에
   있었다. 커밋 로그와 마커 타임스탬프로 오탐임을 확인한 뒤 진행했고,
   경위는 artifact 의 '종결 기록' 에 남겼다.

2) summary --write --force — detached HEAD 라 branch --show-current 가 빈
   값이어서 거부됐다. 이 가드는 feature 브랜치에서 원장을 쓰는 것을 막는
   것인데, HEAD 가 origin/main 과 동일한 sha(8b3beff) 여서 해당하지 않음을
   확인하고 진행했다. 로컬 main 은 다른 워크트리가 점유해 체크아웃할 수
   없었다. 결과는 summary --check 로 '최신 상태 (58개 task)' 확인.

후속 후보(구현 안 함): 끝난 task 를 종결하려면 재활성화해야 하고 그것이
곧 가드 판정 창을 초기화하는 구조라, 앞으로도 같은 오탐이 반복되고
습관적 --force 로 가드가 무력화될 위험이 있다. 판정 창 기준을 switchedAt
대신 meta.created 등으로 잡는 편이 맞다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S79BMNyq1gqXXcEQw1vsnx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants