← 목록
기타 2026-07-12 9KB 읽기 8분

앱상주서버.한선 P0-1b — 스크롤백 파일읽기 O(파일크기) 제거 (재수정 포함)

개요

벡터직결 계약(벡터직결-계약.md) P0 잔여 결함 — P0-1(꼬리 파싱)이 남긴 _출력줄들()/_터미널경로헤드()의 "매 요청 버퍼파일읽기()로 전체 파일을 메모리에 로드"하는 O(파일크기) 비용을 제거한다. 3001줄(278KB) 픽스처 기준 소켓 렌더 p50 52.54ms(구) → 7.21~7.93ms(신), GUI 키 RTT 목표(<10ms) 위반의 근본 원인 해소.

무엇을 했는지 (2단계 수정 — 1차는 자체 발견한 치명 버그로 인해 재수정)

1차 수정 — 파일바이트크기 캐시

_출력줄들()/_터미널경로헤드()에 전역 캐시(_출력캐시_크기/_출력캐시_결과, _경로캐시_크기/_경로캐시_값)를 추가: 매 요청 파일바이트크기()(stat, ~5µs)만 호출해 이전과 크기가 같으면 버퍼파일읽기() 자체를 생략.

⚠️ 자체 발견 P0급 버그 — 배열을 요청 간 캐시하면 안 됨

1차 수정을 500회 순수 벡터|터미널|렌더 요청으로 재현검증한 결과 26/500 "배열 범위 초과(길이 3)" 런타임 에러가 발생(누적 100회 시 VM 자체 중단). 원인: _벡터처리()는 요청마다 메모리마커()로 배열힙 지점을 뜨고 끝에서 메모리복원(마커)으로 그 지점 "이후" 배열힙 전체를 롤백한다(본 파일 자체가 이미 이 패턴에 의존 — 본문은 문자열이라 복원 후에도 안전하게 씀). _출력캐시_결과처럼 배열을 전역 변수에 담아 다음 요청까지 들고 있으면, 다음다다음 요청의 새 배열 할당이 같은 아레나 오프셋을 재사용해 캐시된 배열의 내용/길이 필드를 조용히 오염시킨다(관측: 남의 3원소 배열로 덮여 인덱스 초과). 구버전(P0-1만, 캐시 없음)은 같은 조건에서 0/500 — 이 크래시는 1차 수정이 만든 신규 결함이었다.

재수정: 캐시 대상을 배열이 아니라 문자열(꼬리문자열/경로헤드 값)로 바꿈 — 문자열 풀은 메모리복원()/맵힙복원() 영향 밖(파일 자체가 본문 변수로 이미 증명). 배열(결과)은 캐시 히트/미스와 무관하게 매 요청 그 문자열로부터 새로 만든다.

⚠️ 2차 발견 — 문자열.한선 줄분리()의 은닉 O(n²)

재수정 후 크래시는 사라졌으나(0/500, 0/2500) 3001줄(긴 줄, 90자대) 픽스처 렌더 p50이 여전히 45~46ms — 캐시가 100% 히트 중인데도 개선이 거의 없었다(세부 타이밍 계측으로 확인: 캐시미스 50ms vs 캐시히트 45ms, 버퍼읽기 자체는 겨우 ~5ms). 원인: 재수정 코드가 배열 재생성에 쓰던 줄분리(꼬리문자열)분리()찾기뒤()(문자열.한선)가 내부에서 부분(문자열,i,i+1)을 "문자 위치마다" 반복 호출한다. 부분()/글자()는 매 호출 O(len)(UTF-8 전체 재스캔, 오프셋 캐시 없음 — 이미 알려진 VM 함정)이므로 찾기뒤() 한 번이 O(꼬리길이²), 분리()가 줄 수(~30)만큼 또 반복해 실질 O(30×꼬리길이²)로 폭발(꼬리 ~2700자 기준 수천만~수억 연산). 이 비용은 파일 크기와 무관하게 "렌더되는 텍스트 총 길이"에만 비례 — 원래 진단("52.54ms는 버퍼파일읽기 O(파일크기) 탓")이 부분적 오귀인이었음을 실측으로 확인(짧은 줄 3001줄 픽스처는 구버전에서도 7.95ms로 빠름 — 즉 원래 대조군 두 픽스처가 "파일크기"와 "줄 내용 길이"를 동시에 다르게 잡아 혼입돼 있었음).

최종 수정: _출력줄들()의 배열 재생성 단계에서 줄분리()를 완전히 제거하고, 캐시된(또는 방금 읽은) 꼬리문자열을 문자열버퍼()로 바이트버퍼화 → 버퍼바이트읽기()(O(1)/byte)로 직접 개행을 찾아 줄 하나하나를 버퍼잘라()+버퍼문자열()로 그때그때 변환. 개별 줄에 대한 포함()/부분()은 그 줄 자체가 짧아(수십~백자) 안전 — 반복 대상이 "누적되는 꼬리 전체 문자열"이 아니라 "줄 하나"이므로 폭발하지 않는다(라인 수×평균줄길이에 선형, 제곱 아님). libs/문자열.한선(188개 모듈이 공유하는 라이브러리)은 건드리지 않음 — 문제 회피를 본 파일 내부에서 완결.

검증 (실측, 격리 인스턴스 — 사장님 CrownyAI.app 무접촉, 기존 포트

9801/9802 고아 프로세스 무접촉, 상태파일 전부 사전백업 후 사후 원복)

  1. 컴파일: CROWNY_STRICT=1 hanseonc_high → exit0, [STRICT] 경고 0.
실제 소유 파일 컴파일 결과가 테스트에 쓴 마지막 빌드와 바이트 일치 확인(md5).
  1. 크래시 재현·해소: 500x 순수 벡터|터미널|렌더(3001줄 픽스처) —
1차 수정=26/500 "배열 범위 초과" → 재수정 후=0/500.
  1. 렌더 p50 (3001줄=278KB, 줄당 ~90자 "부하 테스트용 긴 한글 텍스트"
픽스처, 같은 조건에서 구버전과 즉시 비교): - 구버전(P0-1만, 캐시 없음): 51.98~52.54ms - 최종수정본(캐시+버퍼기반 줄분리): 7.21~7.93ms약 6.6~7.3배 개선, 목표(<10ms) 달성. 10줄(소형) 대조군 2.21~2.36ms와 오더 동일(3~4배 이내, 나머지 차이는 실제 렌더되는 TEXT 총량 차이 — 26줄×90자 vs 10줄×20자 — 자체의 불가피한 선형 비용).
  1. 크기변경(append) 감지: 소형 픽스처 렌더 → "MARKER99" 미포함
확인 → 1줄 append → 재렌더 → "MARKER99" 포함 확인(캐시가 파일크기 변화를 정확히 감지해 갱신).
  1. 2500회 스트레스(터미널 KEY/BS/렌더 + 계산기 액션 혼합, 3001줄
픽스처): errs=0/2500, p50=7.93ms, p95=8.70ms, drift_ratio(last100/first100)=1.01x(열화 없음), stderr [STR] 조기경고/런타임 에러 0건(구버전·1차수정본은 각각 [STR] 1회 발생하던 것과 대비 — 처리 속도가 빨라지며 도달 안 함), 프로세스 생존, 종료 verb로 정상 종료(카운터 2509 = 2500+6(레거시 스모크)+2(크기변경검증)+1(종료) 정확 일치).
  1. 레거시 3verb 회귀: 렌더/렌더|960|640/키|FOCUS|.../
키|KEY|x/벡터|계산기|렌더/벡터|터미널|렌더/종료 전부 정상 응답 + 정상 종료.
  1. crownycode: lookup/search 2건 MISS(신규 패턴) → learn add 1건
(앱상주서버_스크롤백_크기캐시).

관련 파일

  • /Users/ef/CrownyBrowser/src/앱상주서버.한선 (단독 소유, 수정 —
_출력줄들()/_터미널경로헤드() + 전역 캐시 변수 4개)
  • /private/tmp/claude-501/-Users-ef/2e95e8b6-e663-4b81-9734-9ded4339b18d/scratchpad/벡터직결-계약.md (SSOT 계약)
  • 검증 스크립트/로그(스크래치, 비영속): p01b_*.py, p01b_*_stderr.log,
디버그_앱상주서버.한선(스크래치 사본, 타이밍/캐시 히트-미스 계측용 — 실소유 파일에는 계측 코드 없음, grep으로 확인)

잔여 이슈 (다음 세션 인계)

  1. [정보] 원 진단 정정: 이전 세션 문서(P0독립검증)의 "52.54ms는
버퍼파일읽기() O(파일크기) 탓" 진단은 실측상 부분적으로만 맞았다 — 실제 지배적 비용은 문자열.한선분리()/찾기뒤()부분()을 문자 위치마다 반복호출해 O(len²)가 되는 것이었고(렌더되는 TEXT 총량에 비례, 파일 크기와 무관), 버퍼읽기 자체는 ~5ms 안팎에 불과했다. 이번 수정으로 두 문제(파일크기 O(파일크기) 읽기 + 콘텐츠 O(len²) 줄분리) 모두 해소.
  1. [P2, 범위 밖] 문자열.한선분리()/찾기()/찾기뒤() 계열
O(n²) 특성: 188개 모듈이 공유하는 라이브러리 함수라 이번 세션 범위(본 파일 단독 소유) 밖 — 큰 문자열(수백~수천자)을 분리()로 반복 스캔하는 다른 서비스도 같은 함정을 가질 수 있음. 근본수정은 찾기()/찾기뒤()를 바이트버퍼 기반(O(1)/byte)으로 재작성하는 것 — libs/문자열.한선 소유팀에 인계 권고.
  1. 이전 세션이 남긴 잔여 이슈(크래시 재기동 트리거 이벤트 유실,
--selftest-inputv2 고아 리크, 미상 고아 프로세스 2건)는 이번 세션 범위 밖 — 그대로 보존.
  1. /tmp/앱상주서버.toau는 이번 수정 반영 전 버전 그대로(ObjC 측
appSrvEnsureToauFresh stale-toau 가드가 소스 mtime 갱신을 감지해 다음 기동 시 자동 재컴파일 — 계약대로 ObjC 담당(P0-2) 영역, 이 세션에서 직접 배치하지 않음). 기존 고아 프로세스(pid 93820:9801, 99334:9802)는 무접촉 유지.