-
Notifications
You must be signed in to change notification settings - Fork 0
Performance Review
본 문서는 pytmux 코어(pytmuxlib/, 약 14k LOC)를 서버 핫패스 · 클라이언트 렌더 · Claude/영속 세 축으로 다시 훑어 도출한 성능 분석 리뷰 보고서다. 직전 처리량 스프린트에서 끝난 항목은 제외하고, 신규(net-new) 최적화 레버만 다룬다.
이 페이지는 설계·근거·결정 중심의 엔지니어링 리뷰다. 실제 측정 수치(머신/OS별 처리량 그래프)는 Performance-Benchmarks에서 라이브로 확인할 수 있으며, 본 문서는 그 벤치마크를 보완한다.
표기: [검증됨] = 작성자가 실제 소스를 읽어 확인. 효과 높음·중·낮음, 위험 낮음·중. 모든 항목은 코드 근거(
file:line)·개선안·효과·위험·검증 게이트를 갖춘다. 측정 우선 원칙은 그대로 — 구현 시scripts/bench.pybefore/after +tests/run.py통과가 게이트다.
pytmux는 단일 서버(데몬)–다중 클라이언트 구조의 터미널 멀티플렉서다. 터미널 출력이 쏟아질 때(빌드 로그, LLM 스트리밍 등) 프레임이 30Hz로 갱신되므로, 프레임당 반복되는 비용이 누적 병목이 된다. 이번 리뷰의 분석 대상은 다음과 같다.
-
렌더 경로: 클라이언트 합성 루프(
client.py의_composite) — 행 × 세그먼트 × 문자 단위로 셀을 blit하는 가장 빈번한 핫패스. -
레이아웃 / 직렬화: 탭바 기하 계산(
clientwidgets.py), 서버 status 메시지 빌드·JSON 인코딩(serverio.py). - 터미널 출력 처리량: 스트리밍 중 status_changed가 30Hz로 서면서 발생하는 반복 직렬화·브로드캐스트·ssh 트래픽.
- 핫루프: 문자당 폭 계산, 전 패널 순회 합산, 프레임/셀마다 재할당되는 불변 객체.
아래 Project-Overview의 다중 패널·탭 구성처럼 패널과 탭이 많을수록(특히 LLM 출력이 매 프레임 변하는 패널) 이 비용들이 곱셈으로 커진다.
| 순위 | 레버 | 위치 | 효과 | 위험 | 비고 |
|---|---|---|---|---|---|
| 1 |
C1 _char_cells 메모이즈 |
clientutil.py:24 |
중~높 | 낮음 | 순수함수, 핫패스 다수에서 문자당 호출 |
| 2 |
C2 TabBar _entries() 프레임당 1회 |
clientwidgets.py:634,684 |
중 | 낮음 | 동일 기하를 프레임당 2회 재계산 |
| 3 | C3 합성 루프 Style/dict 상수 호이스트 | client.py:1063,1078,1162,1190 |
낮음 | 낮음 | 셀/프레임당 불변 Style·dict 재할당 |
| 4 | C4 status 정적 필드 분리(델타) | serverio.py:118 |
중 | 중 | 스트리밍 중 30Hz로 ~55필드 전송 |
| 5 |
C5 _account_token_total 프레임당 1회 |
serverio.py:121,190 |
낮음 | 낮음 | status 빌드서 전 패널 2회 순회 |
권장 착수 순서: C1 → C2 → C3(저위험 즉효 묶음) → C5 → C4(프로토콜 변경, 측정 후).
clientutil.py:24의
def _char_cells(ch: str) -> int:
return 2 if wcwidth(ch) == 2 else 1는 메모이즈가 없다(모듈은 from functools import lru_cache를 이미 import하면서도 이 함수엔 미적용). 이 함수는 렌더 핫패스 곳곳에서 문자 1개당 호출된다.
- 클라 합성 셀 루프(
client.py의 패널 본문 blit — 행 × 세그먼트 × 문자), - TabBar 폭 계산
widths = [sum(_char_cells(c) for c in s) ...](clientwidgets.py:566) + 세그먼트 폭(:658) +active_tab_xrange(:685) → C2의 2회 호출과 곱해진다, - 상태줄 폭 합산(
clientwidgets.py의 누적 폭 계산).
wcwidth(ch)는 코드포인트 테이블 이분탐색이라 문자당 비용이 0이 아닌데, 실 화면은 ASCII(공백·영숫자)가 절대다수라 **캐시 적중률이 거의 100%**다.
개선: 순수함수이므로 @lru_cache(maxsize=256) 한 줄. 입력 도메인이 사실상 유한(자주 쓰는 문자 수백 개)이라 캐시 폭주 없음. 효과: 중~높(텍스트가 빽빽한 패널·다중 탭에서 프레임당 수천 호출의 wcwidth 왕복 제거). 위험: 낮음.
TabBar.render_line(clientwidgets.py:634)과 active_tab_xrange(:684)가 같은 합성 프레임에서 self._entries()를 각각 1회씩 부른다. _entries()(:557)는 매번 _labels()·widths(문자당 _char_cells)·스크롤 보정·항목 루프를 다시 돈다 — 탭이 많을수록(20+) 동일 기하를 프레임당 두 번 계산한다.
주의: _entries()는 self._scroll을 갱신하는 부작용이 있어(:575-580), render_line이 먼저 스크롤을 확정한 뒤 캐시하고 active_tab_xrange가 그 캐시를 읽는 순서를 보장해야 한다. 효과: 중(C1과 곱해 더 큼). 위험: 낮음(스크롤 부작용 순서 보존 필수).
client.py의 _composite 핫패스가 프레임/셀마다 불변 객체를 재할당한다.
-
:1063커서 셀st + Style(reverse=True)—Style(reverse=True)를 프레임마다 새로. -
:1078-1081bbits(11항목)·brev(역인덱스 dict comp)를 매 호출마다 새로 만든다(완전 상수). - 타이틀바·선택 하이라이트
sstl + Style(reverse=True)(선택 셀 수만큼, 대형 선택서 수백~수천 셀).
개선: 불변값을 모듈 상수로 호이스트하고, st → st+reverse는 @lru_cache로 메모(Style은 hashable·immutable). 효과: 낮음(개별 할당은 ns급이나 대형 선택·풀리페인트서 셀당 누적). 위험: 낮음.
_status_msg(serverio.py:118)는 ~55필드 dict를 매 flush(status_changed 시) 통째로 재구성·JSON 인코딩한다. 그중 약 절반(예산/컨텍스트 임계·자동화 옵션·규칙 문자열 등)은 사용자가 설정 팝업에서 토글할 때만 바뀌는 전역 옵션이다. LLM이 스트리밍 중이면 토큰이 매 프레임 변해 status_changed가 30Hz로 서므로, 정적 옵션 ~25개가 초당 30번 재직렬화·재전송된다.
이미 같은 부류의 선례가 있다(prompt-history는 바뀔 때만 전송, 행 델타 전송). status도 같은 패턴 대상이다.
구현 노트(무위험 변형): 코드 확인 결과 ① 토글 핸들러 중 12개(예산/컨텍스트)와 규칙 설정은 이미 변경을 _send_full(full=True)로 회신하고, ② 신규 attach도 full status를 받으며, ③ 클라 update_status가 이미 msg.get(k, self.k)로 키 부재 시 직전 값을 유지한다. 따라서 이 13개를 full status에만 싣고 주기에서 빼면 동작이 증명적으로 불변(클라 코드 변경 0). 가장 무거운 규칙 문자열과 예산/컨텍스트 12필드의 30Hz 재전송이 제거된다. 효과: 중. 위험: 중(프로토콜 분기 — 단, 무위험 변형은 클라 변경 0).
참고(2026-06): 토큰 과사용 완화 제거와 함께
_budget_level_for/budget_level배지 경로가 사라져, 아래 두 번째 순회(예산 레벨 계산)는 이제 존재하지 않는다 — 이 항목은 기록 보존용이다.
_status_msg가 한 번 만들어질 때 _account_token_total이 두 번 전 패널을 순회한다 — 직접(:121)과 _budget_level_for가 내부에서(:190) 한 번 더. 이 함수는 세션→탭→패널 중첩 순회 전체를 돌며 계정 일치분을 합산한다. status가 스트리밍 중 30Hz로 빌드되므로 그 빈도로 2× 전 패널 합산.
개선: 진입에서 합계를 한 번 계산해 claude_tokens·tok5h_pct·budget_level에 공유. 효과: 낮음(10+ 패널·다세션서 합산 절반). 위험: 낮음.
전체 코드 병렬 리뷰가 올린 후보 중, 코드를 직접 읽어 효과/빈도가 과장됐거나 이미 방어된 것들. "추정 ≠ 검증" 원칙으로 기록해 재제안을 막는다.
-
screen_text()가 settled 프레임에도 실행 — 틀림. settled 패널은 텍스트 추출 전에 건너뛴다(_feed_seq == _scan_seq and not pending: continue). 이미 최적. - restart-check의 직렬화 round-trip 왕복 — 의도된 동작. 사용자가 부르는 드라이런(핫패스 아님)이고, 왕복 자체가 "직렬화→역파싱 안전" 점검 항목이다.
-
_save_opts가 토글마다 전체 dump — 사용자 설정 토글(드묾)에서만 호출. 핫패스 아님 + 작은 파일. - 전 패널 합산이 스캔에서 프레임당 호출 — 과장. 단락 평가로 응답 완료 경계·특정 기능 ON에서만 드물게 돈다.
-
usagelog
read(limit)가 전체 파일 로드 — 토큰로그 팝업(모달, 사용자 질의)에서만. 핫패스 아님. - 컨텍스트% 정규식 3종 순차 검색 — 결합 정규식은 가독성·테스트만 해치고 이득 미미(검색당 µs급). 보류.
구현은 레버 1개 = 변경 1건 원칙으로, 각 단계마다 다음을 게이트로 통과시켰다.
python scripts/bench.py # before (baseline)
# ... C1/C2/… 구현 ...
python scripts/bench.py # after — 같은 머신/파라미터
python tests/run.py # 동작 불변(전부 통과)- C1/C2/C3: 클라 렌더 축 — 전 패널 render+직렬화 p50, ptyshot 시각 회귀.
- C4/C5: status 직렬화 바이트·전 패널 합산 횟수, status 값 동일성 회귀.
적용 현황 — C1~C5 전부 구현·게시 완료.
| 레버 | 상태 | 비고 |
|---|---|---|
C1 _char_cells lru_cache |
✅ 구현 |
clientutil.py:24 @lru_cache(256). 회귀 test_char_cells_memoized_correct. |
C2 TabBar _entries() 캐시 |
✅ 구현 | 폭·sel·스크롤·탭 기하 시그니처 프레임 캐시. 스타일은 키 제외(render_line이 매 프레임 재적용). 회귀 test_tabbar_entries_cached_and_consistent. |
| C3 Style/dict 상수 호이스트 | ✅ 구현 |
clientutil.py에 _REVERSE_STYLE·_TB_*·_BOX_BITS/REV 상수 + _with_reverse lru_cache. _composite가 셀/프레임마다 만들던 Style·dict 제거. 회귀 test_with_reverse_and_box_constants. |
| C4 status 정적 옵션 분리 | ✅ 구현 |
_status_msg가 토글 전용 정적 옵션 13개를 full status에만 싣고 주기(full=False)에선 생략. 도달 보장 + 클라 retain-on-omit으로 동작 불변(클라 변경 0). 회귀 test_status_static_opts_only_on_full_c4·test_status_retains_static_opts_when_omitted_c4. |
C5 _account_token_total 1회 |
✅ 구현 |
_budget_level_for(pane, total=None) 선택 인자 추가, 미리 계산한 합계를 넘겨 status 빌드당 전 패널 순회 1회. 회귀 test_budget_level_for_accepts_precomputed_total_c5. |
-
렌더 축(C1~C3): ASCII 위주 화면에서
_char_cells캐시 적중률이 거의 100%로, 프레임당 수천 회의wcwidth왕복과 셀/프레임마다의 Style·dict 재할당이 제거됐다. 다중 탭(20+)에서는 TabBar 기하 계산이 프레임당 2회 → 1회로 줄었다. ptyshot 시각 회귀 동일. - 직렬화 축(C4~C5): 스트리밍 구간(30Hz)에서 status 페이로드의 정적 절반(규칙 문자열 + 예산/컨텍스트 12필드)의 반복 재직렬화·ssh 전송이 사라졌고, 전 패널 토큰 합산이 status 빌드당 2회 → 1회로 줄었다. status 값 동일성 회귀 통과.
라이브 처리량 수치(OS·아키텍처별 p50/p99 등)는 Performance-Benchmarks에서 확인.
소개 · 사용
- Project-Overview
- User-Manual
- Screenshots
- Settings-Popup
- Single-Session-Model
- Tmux-Feature-Comparison
플러그인
Claude Code 플러그인
- Claude-Code-Plugins
- Dev-Guide-claude-code
- Dev-Guide-claude-token-usage-view
- Dev-Guide-claude-prompt-history
- Dev-Guide-claude-resume
- Dev-Guide-claude-disable-feedback
플랫폼 · 성능
품질 · 보안
리뷰·분석 보고서
연혁
기여