크라우니캔버스 컬렉션.한선 O(n²)~O(n³) + OOM 무한루프 근본수리
개요
crowny-canvas(:9620)
한선씨/컬렉션.한선(계약 F4 — 컬렉션/DB 엔진)에서 확증된 P0 결함을 근본 수리.
독립 부하테스트(레코드 300건, hanseonc_high 직접 실행)로 재현한 결과 레코드생성(O(n))은 즉시
완료했으나 목록/필터/정렬 단계에서 3분+ 진행 없이
[ARRAY] OOM!을 무한 반복하며 완주 자체를
못 하는 busy-spin 상태였음(강제 kill 필요). 라이브 :9620는 서버.한선이 컬렉션 함수들을 아직 HTTP
라우트에 결선하지 않아(컬렉션_복원()만 호출) curl로는 재현 불가 — 이 전제는 원 리포트와 일치.
확증된 결함 (수리 전)
컬렉션_PSV파일유니크최신() — 컬렉션_유니크ID목록()이 growing 문자열 방문표+포함하나()
선형탐색(O(n²)),
컬렉션_최신행찾기()가 유니크id마다 전체 행배열 재스캔(O(n²)).
컬렉션_레코드필터()/컬렉션_레코드정렬()/컬렉션_합계()가 레코드id마다 컬렉션_레코드필드JSON()
→
컬렉션_파일최신행()→
컬렉션_PSV읽기()(파일 재읽기)+
컬렉션_최신행찾기()(전체 재스캔)를 반복
호출 — 뷰적용(F4 계약) 1회 요청이 O(n²)~O(n³).
- 부수 근본원인(재현 중 추가 확인):
컬렉션_값비교()가 비교 1회마다 컬렉션_속성타입()→
컬렉션_파일최신행()(배열 반환 함수 체인)을 다시 호출 — 삽입정렬 O(n²) 비교(n=297→약 88,000회)가
매번 배열반환 함수를 재실행해 가드레일 문서화된 "루프 내 배열반환 함수 반복호출 ~200회=
[ARRAY] OOM, 144M셀 힙 고갈, 반환 배열은 메모리마커/복원으로도 회수 안 됨" 함정을 그대로 재현.
수리 내용 (/Users/ef/crowny-canvas/한선씨/컬렉션.한선)
- PSV파일유니크최신: growing 문자열 방문표+유니크id별 재스캔 → 맵생성/맵있나/맵넣어 해시맵
단일패스(O(n)). 검색.한선 검색_질의·서버.한선 온보딩 처리와 동일 관용구.
- 컬렉션_레코드맵빌드(신규): 레코드 파일을 컬렉션id당 "1회만" 읽어 id→필드JSON 맵(+활성순서
배열)을 구축. 필터/정렬/합계/목록이 전부 이 단일 빌드 결과만 사용(맵꺼내=O(1)).
- 속성타입 루프밖 호이스팅:
컬렉션_값비교타입(속성타입,a,b)(순수, 파일접근 없음) 신설.
필터/정렬이 루프 진입 "전" 속성타입을 1회만 조회해 캐시하고, 비교는 이 순수함수만 사용 —
O(n²) 삽입정렬 비교 내부에서 배열반환 함수가 재호출되는 경로를 완전히 제거(OOM 근본원인 제거).
- 빌드 재사용(3단계):
컬렉션_레코드필터_코어/컬렉션_레코드정렬_코어/컬렉션_합계_코어를
분리해
컬렉션_뷰적용()(F4 "1회 요청")과
컬렉션_전체합계()가 파일을 2번씩 읽던 것을 1번으로
축소(공용 인자로 이미 구축된 필드맵/활성순서를 그대로 전달).
- (시도 후 되돌림) 저장소.한선의 버퍼기반(BUF_*) PSV파싱+단일행 필드캐시 관용구를 잔여
O(파일크기²)(글자()/부분() 매호출 O(len) 재스캔) 완화용으로 이식 시도했으나, 이 파일의 실사용
패턴(추가()/문자열 연결식 안에 버퍼경유 함수를 깊이 중첩 호출 — 자체테스트에 이미 존재)에서
가드레일 문서화된 "조건식 내 버퍼IO 함수 인라인=평가 뒤집힘/소실" 함정이 재현됨(정렬금액들
배열이 항상 길이 0으로 소실, 최소재현 3종으로 격리). 성능 이득보다 침묵 데이터손실 위험이 커
즉시 되돌림 — 분리()/글자() 기반 원래 구현 유지. 버퍼기반 전환은 별도 세션에서 이 파일 전체
호출부(추가/연결식 내 중첩 호출)를 전수 변수분리한 뒤 재시도 필요(잔여 이슈로 남김).
검증
- STRICT 재컴파일:
컬렉션.한선 단독 + 서버.한선(10개 모듈 전체 import 체인, 77622 큐브) 0경고.
- 자체테스트(
CANVAS_COLLECTION_SELFTEST=1): 7/7 PASS(속성순서·레코드3건·필터2건·정렬순서·
합계250000·뷰(필터+정렬)결과·바인딩 쓰기정책) — 수리 전/중간 단계/최종 각각 재확인.
- 부하테스트(레코드 300건,
crownycode-learn 재현 스크립트와 동일 시나리오):
| 단계 | 결과 |
|---|---|
| 수리 전 | 레코드생성 완료(수초) → 목록/필터/정렬 단계 3분+ 무진행,
[ARRAY] OOM! 무한반복(강제kill) |
| 1차 수리(맵빌드 도입) | 레코드필터까지 정상 진행(4ms) → 정렬 진입 직후 재차 OOM(속성타입 배열반환 함수 O(n²) 재호출이 근본원인이었음, 추가 발견) |
| 2차 수리(속성타입 호이스팅) |
완주: 목록 3ms·필터 4ms·정렬 4ms·합계 6ms·뷰적용 8ms, 총 25ms(내부 타이머). 벽시계 종단 ~25s(문자열.한선 분리()/부분() O(len) 재스캔 다중호출 누적 — VM 공유 프리미티브 한계, 별도 이슈) |
| 3단계(빌드 재사용, 7→5회) | 동일 정합, 벽시계 ~18s(30% 단축) |
| 버퍼기반 시도 | 자체테스트 회귀(정렬금액들 소실) → 즉시 롤백, 3단계 상태로 최종 확정 |
- 라이브 재확인:
서버.toau 재컴파일(0경고) → launchctl kickstart -k gui/$(id -u)/org.crowny.canvas
→
/·
/api/부팅 200 확인(재기동 후 정상 기동).
관련 파일
/Users/ef/crowny-canvas/한선씨/컬렉션.한선 (수리 본체)
/Users/ef/crowny-canvas/서버.toau (재컴파일 반영)
/Users/ef/crowny-canvas/docs/00-작업계획서-에이전트디렉팅.md, docs/07-앱-계약.md (계약 SSOT, 무수정)
- LaunchAgent
org.crowny.canvas (kickstart로 재기동만, plist 무수정)
잔여 이슈
문자열.한선(공유 표준라이브러리, 이 파일 소유권 밖)의 분리()/찾기()/찾기뒤()가 내부적으로
부분() 위치별 반복호출이라 대형 문자열에서 O(n²) — 저장소.한선과 동일 근본원인. 이 파일은
버퍼기반 우회 시도가 자체테스트 회귀를 일으켜 되돌렸으므로 여전히 이 비용을 그대로 짊어짐
(n=300에서 ~18s). 별도 세션에서 파일 전체 호출부를 "추가()/문자열연결식 안에 함수 중첩 호출
금지 → 변수로 먼저 받기" 원칙으로 전수 정리한 뒤 버퍼기반 재도입 검토.
컬렉션.한선은 아직 서버.한선에 결선되지 않음(HTTP 라우트 없음, 컬렉션_복원()만 호출) —
이번 수리는 파일 소유권 범위(컬렉션.한선) 내 결함 수리만 수행, 결선은 별도 작업(F6 트랙).
컬렉션_ID생성()이 현재시간()+무작위(1000,9999) 조합이라 같은 시각·같은 프로세스에서 대량
생성 시 드물게 ID 충돌(레코드 300건 생성 시 실측 4~5건 중복, latest-wins로 조용히 병합됨) —
이 리포트의 O(n²) 범위 밖이라 별도 확인 필요.