벤치마크
모든 측정값이에요. 10배에 못 미친 항목(⛔)과 그 이유, 시도했다가 넣지 않은 것까지 그대로 적어요. README에는 이 중 한 가지 규칙으로 범위 전체가 10배 이상인 항목만 올려요.
요약: 전체 항목
측정한 15개 항목 중 11개는 범위 전체가 tmux보다 10배 이상 좋아요. 나머지 4개(⛔)는 범위의 아래 끝이 1.6~8.9배예요. 하한의 원인은 아래 10배 목표 현황에 적었어요. 모든 값은 같은 기기(Mac mini M1)에서 tmux와 번갈아 잰 실측이에요.
11개 중 하나는 tmux가 느려져서 10배를 넘었어요. 5.4MB 출력이 화면에 보이기까지는 2026-10-18에 경계(9.9~11.7×)였고 2026-10-21에는 10.5~12.5×예요. zeropty는 두 날 모두 같았고(14~16ms, 그다음 15~16ms), tmux가 더 바쁜 기기에서 159~164ms에서 169~188ms가 됐어요. 규칙상 ✅이지만 README에는 올리지 않아요.
| 무엇을 | tmux | zeropty | 배수 | 줄어든 비율 |
|---|---|---|---|---|
| CPU, 실제 claude 작업 400개 | 54.30~54.91% | 3.51~3.84% | 14.1~15.6× | −92~93% |
| 메모리, 실제 claude 작업 400개 (독립 서버) | 32.4~32.5MB | 2.66~2.70MB | 12.0~12.2× | −91% |
| 메모리, 실제 claude 작업 400개 (임베디드, polled) | 32.4~32.5MB | 1.63~1.64MB | 19.7~19.9× | −94% |
| 메모리, 대기 400개 (임베디드, polled) | 8.0MB | 0.45~0.46MB | 17.3~17.7× | −94% |
| 메모리, 대기 200개 (임베디드 + 서버 플러그인, CLI·attach로도 접근) | 5.0~5.1MB | 388~389KB | 12.8~13.1× | −92% |
| 메모리, 화면 갱신 400개 | 251.2~251.4MB | 1.71~1.74MB | 144~147× | −99% |
| 메모리, 대기 400개 (독립 서버) | 8.0MB | 1.25~1.27MB | 6.2~6.4× ⛔ | −84% |
send-keys, tmux 명령 실행 vs zeropty 소켓 (Node) | 9.03~9.56ms | 0.24~0.26ms | 34.7~39.8× | −97% |
send-keys, tmux 제어 모드(-C) vs zeropty 임베디드 (Node) | 0.42~0.49ms | 0.06~0.07ms | 6.0~8.1× ⛔ (회차별 7~8× †) | −83~87% |
send-keys, tmux 제어 모드(-C) vs zeropty 소켓 (Node) | 0.42~0.49ms | 0.24~0.26ms | 1.6~2.0× ⛔ (회차별 1.7~2.0× †) | −38~51% |
키 입력 → 화면, tmux 제어 모드(-C) vs zeropty 임베디드 (Node) | 0.89~0.98ms | 0.09~0.10ms | 8.9~10.8× ⛔ (회차별 9~10× †) | −88~90% |
셸에서 has-session, tmux vs zeropty zsh 함수 | 6.04~6.19ms | 0.11~0.17ms | 35.5~56.2× | −97~98% |
셸에서 send-keys, tmux vs zeropty bash builtin | 6.02~6.31ms | 0.043~0.048ms | 125~146× | −99% |
| 5.4MB 출력, 프로그램이 끝나기까지 | 166~176ms | 3.0~4.6ms | 36.0~58.6× | −97~98% |
| 5.4MB 출력, 화면에 보이기까지 | 169~188ms | 15~16ms | 10.5~12.5× | −90~92% |
| 세션 수 한도 | 511개 (PTY 기본 한도, 최대 999) | 1000개 확인, PTY 한도 없음 | – | – |
배수 계산 방법
이 문서의 모든 배수와 줄어든 비율은 한 가지 규칙을 따라요.
- 표에 적힌 값 범위의 양끝으로 계산해요. 아래 끝은 가장 빠른(작은) tmux 값 ÷ 가장 느린(큰) zeropty 값, 위 끝은 그 반대예요. 값이 하나면 배수도 하나예요. 회차끼리 짝지은 값은 남기지 않았으니, 표만 보고 누구나 같은 배수를 다시 계산할 수 있게 했어요.
- 올리지 않고 버려요. 100배 미만은 소수 첫째 자리 아래를, 100배 이상은 소수점 아래를 버려요(13.96 → 13.9, 141.6 → 141). 줄어든 비율도 정수로 버려요. 단위가 섞이면(MB와 KB) 1MB = 1000KB로 계산해 작은 쪽 배수를 적어요.
- 10배 판정도 이 규칙 하나로 해요. 범위의 아래 끝이 10배 이상이어야 ✅이고 README에 올라가요. 아래 끝이 10배 미만이면 위 끝이 10배를 넘어도 ⛔예요.
- 참고로만 적는 값:
bench/latency.mjs가 직접 출력한 배수(†). 스크립트는 회차마다 같은 회차의 반올림 전 p50끼리 나눈 배수를 출력해요(임베디드 열은 정수로, 소켓 열은 소수 첫째 자리로 반올림). Node 응답 속도 표의 오른쪽 세 열과 동기 클라이언트 배수가 그 세 회차의 범위예요. 회차별 p50 값은 남기지 않아서 표만으로는 다시 계산할 수 없으니, 판정에는 쓰지 않고 † 표시를 붙여 함께 적기만 해요. 예를 들어 임베디드send-keys(tmux 실행 대비)는 회차별로 140~151×이고, 값 범위 양끝으로 나누면 128~159×예요. 키 입력 → 화면(tmux -C 대비, 임베디드)은 어떤 회차에서는 10×였지만(회차별 9~10×) 값 범위 양끝으로는 8.9~10.8×라 ⛔예요.
이 문서의 수치는 항상 최신 측정값이에요. 수치가 바뀌는 변경은 같은 커밋에서 이 문서와 README의 표를 갱신해요.
측정 환경
| 항목 | 값 |
|---|---|
| 날짜 | 2026-10-21 (서버 메모리·CPU, 서버 플러그인을 켜고 끈 임베디드 메모리, Node 응답 속도 세 번, 셸, 대량 출력, wait-pane, grep-panes), 2026-10-19 (C 클라이언트·서버 플러그인 응답 속도, polled 이벤트 루프 막힘) |
| 커밋 | 9694aa3에 이 변경의 bench/ 스크립트 (2026-10-21 측정) |
| 기기 | Apple M1 (P코어 4 + E코어 4), 16GB |
| OS | macOS 26.6.2 |
| 비교 대상 | tmux 3.7c |
측정 조건
- 세션 설정: 220×80, 스크롤백 2000줄. tmux도 같은 크기이고, 스크롤백은 tmux 기본값 2000줄이에요.
- 메모리: 20초 동안 2초마다 잰 footprint 10개의 평균이에요.
- CPU: 같은 구간의 실제 경과 시간으로 나눈 값이에요.
- 실제 작업 재생: 세션 i가 녹화본의 (i × 0.618 mod 1) 지점부터 시작해서, 회차와 서버가 달라도 같은 부하예요.
- 같은 설정을 반복하면 메모리는 ±1.5%, CPU는 ±5% 정도 흔들려요(서버가 효율 코어에서 도는 일이 많아요).
- 2026-10-21에는 기기에서 다른 프로그램이 돌고 있었어요(load average 약 2.9). 그래서 응답 속도와 셸 측정에서 tmux와 zeropty 모두 이전 날짜보다 느렸어요. 그래도 짝은 모두 같은 부하에서 번갈아 쟀고, 배수는 그 짝끼리 비교한 값이에요.
메모리 · CPU: 독립 서버 (bench/suite.sh)
| 부하 | 세션 | tmux 메모리 | zeropty 메모리 | 배수 | tmux CPU | zeropty CPU | 배수 |
|---|---|---|---|---|---|---|---|
| 대기 (claude 화면 1장 출력 후 정지) | 200 | 5.0~5.1MB | 1.20MB | 4.1~4.2× ⛔ | 0% | 0% | – |
| 400 | 8.0MB | 1.25~1.27MB | 6.2~6.4× ⛔ | 0% | 0% | – | |
| 화면 갱신 (1초마다 31줄 다시 그림) | 200 | 121.0~121.3MB | 1.57~1.61MB | 75.1~77.2× | 18.21~18.31% | 1.02~1.12% | 16.2~17.9× |
| 400 | 251.2~251.4MB | 1.71~1.74MB | 144~147× | 28.71~29.79% | 1.85~2.09% | 13.7~16.1× | |
| claude 기록 통째 출력 (15KB, 2초마다) | 200 | 7.3~7.7MB | 1.71~1.73MB | 4.2~4.5× ⛔ | 26.74~26.83% | 1.51~1.61% | 16.6~17.7× |
| 400 | 12.5~13.6MB | 1.95~2.30MB | 5.4~6.9× ⛔ | 42.29~42.82% | 2.96~2.97% | 14.2~14.4× | |
실제 claude 작업 재생 (tests/streams/claude2.trace) | 200 | 17.4MB | 1.65~1.66MB | 10.4~10.5× | 36.04~36.67% | 2.24~2.29% | 15.7~16.3× |
| 400 | 32.4~32.5MB | 2.66~2.70MB | 12.0~12.2× | 54.30~54.91% | 3.51~3.84% | 14.1~15.6× | |
| 대기 | 1000 | 불가 (PTY 한도) | 1.38~1.39MB | – | 불가 | 0% | – |
| 실제 claude 작업 재생 | 1000 | 불가 (PTY 한도) | 4.65MB | – | 불가 | 6.69~7.10% | – |
2026-10-21에 bench/suite.sh를 두 번 돌린 값이고, 범위는 두 회차예요. 여기서 MB는 footprint KB ÷ 1000이에요.
- 실제 작업 200개가 이제 10배를 넘어요(2026-10-19에는 8.4~9.0×). tmux는 17.4MB 그대로인데 zeropty가 1.92~2.05MB에서 1.65~1.66MB가 됐어요. claude 기록 통째 출력 200개의 CPU는 경계(9.9~10.2×)에서 16.6~17.7×가 됐어요. tmux CPU가 늘고(17.9% → 26.74~26.83%) zeropty는 줄었어요(1.75~1.80% → 1.51~1.61%). 둘 다 원인은 찾아보지 않았어요.
세션 1000개의 출력이 다 흐르게 고쳤어요(2026-10-18). 이전 측정(4.66%)은 출력의 59%만 흐르던 상태였어요. 원인은 macOS 파이프였어요.
- macOS는 파이프 버퍼를 시스템 전체 16MB에서 나눠 줘요. 64KB까지 커진 파이프가 약 250개 생기면, 나머지 파이프는 모두 512바이트만 받아요(직접 만든 파이프 3000개로 확인).
- 512바이트 파이프는
fstat이나 kevent로도 구별되지 않아요(둘 다 16KB라고 해요). 그런 세션의 프로그램은 300ms 읽기마다 512바이트만 쓰고 기다렸어요. - 세션 출력을 socketpair(버퍼 64KB, 세션마다 따로)로 바꾸자, 1000개 재생에서 서버가 읽은 양이 녹화 속도 대비 59%에서 **102%**가 됐어요. 200·400개는 전후 모두 101%예요.
- 그래서 1000개의 CPU는 더 많은 출력을 처리한 값이에요.
- 대가: zeropty가 읽기를 멈추면 세션마다 최대 64KB가 커널 소켓 버퍼(mbuf)에 쌓여요. 1000개면 64MB예요(이 기기의 mbuf 풀은 256MB). 파이프는 시스템 전체 16MB가 상한이었어요.
⛔ 표시는 서버 프로세스 하한 때문에 10배가 안 되는 항목이에요. 세션 1개인 zeropty 서버도 1.09MB예요(2026-10-19). 아무것도 안 하는 C 프로그램이 0.87MB, perl 프로세스가 1.36MB, tmux 빈 서버가 2.2MB예요.
- 빈 C 프로그램 0.865MB도 대부분 운영체제 몫이에요(
footprint로 분해): 커널 페이지 테이블 209KB(XNU는 이것도 footprint에 넣어요), 시스템 malloc 메타데이터 약 200KB, 라이브러리가 고쳐 쓴 데이터(__DATA_DIRTY,__DATA, shared cache) 약 240KB, 스택과 malloc 영역. 링크 옵션(-no_fixup_chains,-dead_strip, 작은 스택)으로는 잡음 수준(±16KB)만 움직였고, Apple의/bin/sleep도 832KB예요. - 줄일 수 있었던 건 시스템 malloc 설정이에요(2026-10-19 적용). 서버는
MallocNanoZone=0 MallocMaxMagazines=1 MallocSpaceEfficient=1 MallocMediumZone=0으로 자신을 다시 실행해요(libmalloc은 시작할 때만 읽어요). 화면은 mimalloc이 맡고 시스템 malloc은 libSystem 몫만 하니, 스레드 하나에 맞게 영역과 CPU별 캐시를 줄여도 돼요. 모든 부하에서 144~240KB가 줄었어요(세션 1개 1233 → 1089KB, 대기 200개 1.39~1.44 → 1.23MB, 실제 작업 400개 2.96~3.04 → 2.72~2.82MB, 번갈아 2회). CPU는 같은 분포였어요(실제 작업 400개, 번갈아 3회: 1.47/1.53/4.01% vs 1.47/3.85/3.86%, 서버가 어느 코어에서 도느냐로 두 값을 오가요). 세션의 프로그램에는 이 설정을 물려주지 않아요. - zeropty 몫은 mimalloc이 잡은 384KB예요. 실제 힙은 8KB인데, 오래 사는 작은 객체들이 크기 등급별로 다른 16KB 쪽에 흩어져 쪽마다 통째로 잡혀요. mimalloc 설정(eager commit, arena 크기)은 효과가 없었어요.
MallocNanoZone=0하나만으로는 16KB뿐이고, 위의 네 설정을 함께 써야 줄어요. 예를 들어 대기 400개가 10배가 되려면 0.80MB 이하여야 해서 빈 프로세스보다 작아야 해요.
서버를 다른 언어로 쓰면 넘을 수 있을까요? 빈 서버를 직접 만들어 쟀어요(2026-10-18, Mac mini M1).
| 빈 서버 | 메모리 |
|---|---|
| 시스템 콜만 쓰는 정적 바이너리 (libSystem 없음) | 실행 불가: Apple Silicon macOS 커널이 바로 SIGKILL해요 |
C, pause()만 / kqueue와 소켓 서버 | 0.865MB / 0.865MB |
Rust, 빈 main | 0.93MB |
| zeropty 빈 서버 | 1.15MB |
| Go, 소켓 서버 | 2.59MB |
C로 다시 짜면 빈 서버에서 약 0.29MB를 아낄 수 있어요. 하지만 C 빈 프로세스(0.865MB)가 이미 대기 400개의 10배 기준(0.80MB)보다 커요. 그래서 별도 프로세스로는 어떤 언어로도 불가능해요. 이 하한을 넘는 방법이 아래 임베디드 모드예요. 따로 프로세스를 두지 않으니 프로세스 하한이 없어요.
세션 수가 200에서 400으로 늘 때 세션 하나당 늘어난 메모리 (독립 서버):
| 부하 | tmux | zeropty | 배수 |
|---|---|---|---|
| 대기 | 14.9~15.0KB | 0.24~0.32KB | 46.5~62.5× |
| 화면 갱신 | 649~652KB | 0.54~0.83KB | 781~1207× |
| claude 기록 통째 출력 | 23.8~31.8KB | 1.1~3.0KB | 7.9~28.9× ⛔ |
| 실제 claude 작업 | 74.8~75.3KB | 5.0~5.3KB | 14.1~15.0× |
두 회차(2026-10-21)의 footprint KB로 계산했고, 다른 곳처럼 범위 양끝을 짝지었어요. claude 기록 통째 출력이 ⛔인 건 zeropty 400개 footprint가 두 회차에서 352KB 달랐기 때문이에요(1948KB와 2300KB). 2026-10-19에는 13.2×였어요.
PTY 사용량: 세션 400개일 때 tmux는 435개, zeropty는 35개예요. 35개는 시스템 기본값이라 세션 1000개에서도 그대로예요.
메모리: 임베디드 (엔진을 내 프로세스 안에, bench/embed_host.c)
다른 일을 하지 않는 C 프로그램에 엔진을 넣고, 엔진을 켜기 전과 세션을 띄운 뒤의 footprint 차이를 쟀어요. Node처럼 자체 힙이 수 MB씩 움직이는 호스트에서는 이렇게 정확히 잴 수 없어서 C 호스트를 써요.
엔진은 두 방식으로 돌 수 있어요. 엔진 스레드(zeropty_open, Node { polled: false })는 엔진이 자기 스레드를 가져요. polled(zeropty_open_polled, Node 기본값)는 스레드 없이 호스트의 이벤트 루프가 엔진을 돌려요. 값은 2026-10-21에 방식마다 두 번 잰 범위이고, tmux 서버 열은 위 독립 서버 표의 값이에요.
| 부하 | 세션 | tmux 서버 | zeropty 엔진 스레드 | 배수 | zeropty polled | 배수 |
|---|---|---|---|---|---|---|
| 대기 | 200 | 5.0~5.1MB | 0.68~0.69MB | 7.2~7.5× ⛔ | 0.39MB | 12.8~13.0× |
| 400 | 8.0MB | 0.74MB | 10.8× | 0.45~0.46MB | 17.3~17.7× | |
| 실제 claude 작업 재생 | 200 | 17.4MB | 1.65~1.66MB | 10.4~10.5× | 0.95~1.32MB | 13.1~18.3× |
| 400 | 32.4~32.5MB | 2.25~2.33MB | 13.9~14.4× | 1.63~1.64MB | 19.7~19.9× |
- 엔진 스레드를 켜는 것만으로 +308~309KB예요. 스레드의 할당기 힙, 스택, 페이지 테이블 몫이에요. polled는 +180~181KB예요.
- polled 대기 200개가 0.31MB(16.4×, 2026-10-18)에서 0.39MB(12.8~13.0×)가 됐어요. 엔진 시작이 +149KB에서 +180~181KB로, 세션 200개 몫이 +160KB에서 +208KB로 늘었어요. 그 사이 어떤 변경이 늘렸는지는 아직 찾지 못했어요.
- polled는 세션 몫도 작아요. 모든 할당이 호스트 스레드의 힙 하나에 모이기 때문이에요(대기 200개: 368~384KB → 208KB).
- polled의 비용: 세션 출력 파싱을 호스트 스레드가 해요(실제 작업 400개에서 코어 하나의 ~3.5%). 명령은 모두 그 스레드에서 불러야 해요(Node는 원래 JS 스레드 하나라 제약이 없어요. worker 스레드에서 부를 때만
{ polled: false }). - 대량 출력은 조각내서 파싱해요. polled는 출력 덩어리를 256KB씩, 한 번에 최대 2ms만 읽고 호스트 루프에 차례를 돌려줘요. 바쁜 세션 200개에 5.4MB 출력이 들어올 때 Node 이벤트 루프가 가장 길게 막힌 시간이 11~32ms에서 3~6ms가 됐어요(
bench/loop_block.mjs, 번갈아 3회). 화면에 다 들어오는 시간과 메모리는 그대로예요. 엔진 스레드 방식도 CPU를 두고 다투느라 최대 30ms 막혔어요. - 세션 하나당은 대기 0.24~0.32KB, 실제 작업 3.0~3.4KB예요(엔진 스레드 방식, 세션 200 → 400개).
- 트레이드오프: 10초 넘게 조용한 화면은 zstd로 압축해 이름 없는 임시 파일에 둬요. 프로세스 메모리에서는 빠지지만 OS 페이지 캐시에는 남아요(필요하면 OS가 회수해요).
ZEROPTY_COLD=memory로 메모리에 두게 할 수 있어요.
서버 플러그인을 켜면 (listen, bench/embed_host.c ... polled <소켓>, 2026-10-21, 켬·끔 번갈아 2회). 같은 C 호스트에서 소켓을 열었을 때와 안 열었을 때를 쟀어요.
| 부하 | 세션 | tmux 서버 (위 표) | 플러그인 없음 | 플러그인 켬 | tmux 서버 대비 (켬) |
|---|---|---|---|---|---|
| 엔진만 | 0 | – | +180~181KB | +180~181KB | – |
| 대기 | 200 | 5.0~5.1MB | 388KB | 388~389KB | 12.8~13.1× |
| 실제 claude 작업 재생 | 200 | 17.4MB | 949~1322KB | 973~1248KB | 13.9~17.8× |
| 400 | 32.4~32.5MB | 1630~1636KB | 1395~1740KB | 18.6~23.2× |
- 플러그인 자체는 이제 0~1KB예요(2026-10-19에는 소켓 하나와 연결 목록으로 +16~18KB). 같은 기간에 polled 시작 비용이 약 31KB 늘었으니,
listen이 더하던 것을 이제 엔진이 시작할 때 마련하는 것일 수 있어요. 확인하지는 않았어요. 실제 작업 부하의 차이는 회차마다 위아래로 바뀌는 잡음 범위예요. - 그래서 CLI·attach·소켓 클라이언트를 쓰면서도 메모리는 임베디드 수준이에요. 같은 대기 200개를 독립 서버로 두면 1.20MB(4.1~4.2× ⛔)예요. 차이는 서버 프로세스 하나의 몫이에요.
- 주의: 이 값은 호스트 프로그램이 원래 있다는 전제의 증가분이에요(호스트 자체 ~1.08MB 제외). 세션만 두려고 호스트를 따로 띄운다면 그건 독립 서버와 같아요.
응답 속도 (node bench/latency.mjs 200)
Node 프로그램이 세션 서버를 부르는 네 가지 방식을 나란히 쟀어요.
| 방식 | 설명 |
|---|---|
| tmux | Node execFile로 명령마다 tmux 실행 (프로그램이 tmux를 부르는 흔한 방식) |
| tmux -C | 제어 모드: tmux 클라이언트 하나를 계속 열어 두고 stdin으로 명령 전송 (tmux를 가장 빠르게 쓰는 방법) |
| zeropty | 연결 하나를 유지하며 서버 소켓에 직접 요청 (client/zeropty.mjs) |
| zeropty 임베디드 | 엔진을 Node 프로세스 안에 넣고 함수로 호출 (client/embedded.mjs) |
서버마다 바쁜 세션 200개를 띄우고, 요청을 번갈아 보냈어요. 항목마다 40회씩이고, 2026-10-21에 세 번 실행한 p50(ms)의 범위예요. 임베디드 열은 Node 기본값인 polled 방식이에요. 오른쪽 세 배수 열은 bench/latency.mjs가 회차마다 출력한 배수의 범위(†)예요. 10배 판정은 그 아래 표의, 값 범위 양끝으로 나눈 배수로 해요(배수 계산 방법).
| 항목 | tmux | tmux -C | zeropty | zeropty 임베디드 | 임베디드 vs tmux † | 임베디드 vs tmux -C † | zeropty vs tmux -C † |
|---|---|---|---|---|---|---|---|
capture-pane (바쁜 세션) | 9.31~9.90 | 0.55~0.63 | 0.25~0.30 | 0.10~0.17 | 57~93× | 3~5× ⛔ | 1.8~2.5× ⛔ |
capture-pane -e -S -200 (조용한 세션) | 9.37~10.14 | 0.45~0.60 | 0.23~0.27 | 0.06~0.07 | 134~172× | 7~10× ⛔ | 2.0~2.3× ⛔ |
send-keys | 9.03~9.56 | 0.42~0.49 | 0.24~0.26 | 0.06~0.07 | 140~151× | 7~8× ⛔ | 1.7~2.0× ⛔ |
| 키 입력 → 화면에 보이기까지 | 24.62~25.24 | 0.89~0.98 | 0.35~0.43 | 0.09~0.10 | 250~268× | 9~10× ⛔ (값 범위로는 8.9~10.8×) | 2.1~2.8× ⛔ |
같은 p50 범위를 한 가지 규칙(값 범위의 양끝으로 나누고 버림)으로 계산한 배수예요. 10배 판정은 이 표로 해요.
| 항목 | zeropty vs tmux | 임베디드 vs tmux | 임베디드 vs tmux -C | zeropty vs tmux -C |
|---|---|---|---|---|
capture-pane (바쁜 세션) | 31.0~39.6× | 54.7~99.0× | 3.2~6.3× ⛔ | 1.8~2.5× ⛔ |
capture-pane -e -S -200 (조용한 세션) | 34.7~44.0× | 133~169× | 6.4~10.0× ⛔ | 1.6~2.6× ⛔ |
send-keys | 34.7~39.8× | 128~159× | 6.0~8.1× ⛔ | 1.6~2.0× ⛔ |
| 키 입력 → 화면에 보이기까지 | 57.2~72.1× | 246~280× | 8.9~10.8× ⛔ | 2.0~2.8× ⛔ |
임베디드 열은 polled 방식이고, 엔진 스레드 방식도 같은 수준이에요. 2026-10-21에 번갈아 두 번씩 잰 polled 대 엔진 스레드 p50: 바쁜 세션 capture 0.08~0.09 vs 0.08~0.11, 조용한 세션 capture 0.06~0.07 vs 0.05~0.07,
send-keys0.06 vs 0.06, 키→화면 0.10 vs 0.08~0.09. 그러니 임베디드send-keys가 0.04~0.06ms(2026-10-18, 엔진 스레드)에서 0.06~0.07ms가 된 건 방식 때문이 아니에요. 기기가 더 바빴고, 더 확인하지는 않았어요.임베디드
send-keys(tmux -C 대비)가 라인 디시플린을 넣으며(2026-10-18) 회차별 10~11배에서 8~10배가 됐고(†, ⛔), 2026-10-21에는 회차별 7~8배예요. 측정 세션(cat)이 이제 PTY처럼 cooked 모드예요. 그래서 키마다 모드 확인 읽기 한 번과 에코 처리가 더해졌어요(키 하나에 약 10µs). tmux도 같은 에코를 처리하지만 서버 쪽에서 send-keys가 돌아간 뒤에 해요. raw 모드 세션(claude)에는 더해지는 일이 없어요(examples/sendkeys_bench.rs, 변경 전과 같음). shim이 모드를 출력 대신 공유 메모리로 알려 주면 모드 확인 읽기를 없앨 수 있지만 넣지 않았어요. 얻는 건 cooked 세션의 키 하나당 약 8µs(25ms 간격, cooked 22.9µs vs raw 14.7µs)뿐이고, 실제 대상인 claude는 이미 raw예요.임베디드
capture-pane(바쁜 세션)이 크게 흔들리는 이유를 쪼개 봤어요(회차별 2026-10-18 5~12배, 2026-10-21 3~5배. 분석은 2026-10-18, 바쁜 세션 200개).- 같은 화면을 연달아 capture하면 5.9µs예요. 25ms 간격으로 부르면 렌더링만 27µs예요. 쉬다 깨어난 CPU의 클럭과 캐시 때문이에요. 세션 1개로 재도 같았고, 부르는 스레드의 QoS를 올려도 거의 같았어요.
- 바쁜 세션은 300ms 동안 모은 출력을 capture가 그 자리에서 파싱해요. p50 15~30µs, 8~12KB가 쌓였으면 최대 0.5ms예요. 엔진 스레드가 다른 세션을 읽는 중이면 잠금을 기다려요(p99 최대 0.5ms).
- 바뀌지 않은 화면의 capture 결과를 저장해 두면 식은 상태에서도 렌더링이 빠지지만, 넣지 않았어요. 화면 하나에 약 17KB라 세션 1000개면 17MB가 늘어요.
- 최근 capture한 세션을 바로 읽게 하면 이 부분이 사라지지만, 넣지 않았어요. 폴링하는 호스트는 폴링 간격(수백 ms)이 지배적이라 체감 이득이 없고, CPU가 늘어요(바로 읽기: 세션 200개에 +0.8%p). 빨리 알아야 하는 경우는
wait-pane이 있어요.
배수가 흔들리는 이유: tmux -C가 0.4~0.6ms, 임베디드가 0.05ms 안팎이라 0.01ms 차이에도 배수가 2~3씩 움직여요. 그래서 한 번의 값이 아니라 범위로 적었어요. 바쁜 세션 capture는 그동안 쌓인 출력을 먼저 읽고 화면을 그리는 비용이 들어 있어요.
zeropty 소켓 방식은 명령마다 tmux 실행 대비 31.0~72.1배예요(위 두 번째 표의 첫 열).
소켓 방식(zeropty)은 tmux -C 대비 1.6~2.8배예요(⛔, 위 두 번째 표의 마지막 열. 회차별로는 1.7~2.8× †). Node에서 Unix 소켓 왕복 자체가 하한이에요(아무 일도 안 하는 에코 서버가 0.33ms).
C 프로그램에서 재도 비율은 1.7~3.3배예요(
bench/latency_c.c, 2026-10-18). Node를 빼면 tmux -C도 같이 빨라지기 때문이에요.
| C 클라이언트에서 (p50) | tmux -C | zeropty 소켓 | 배수 |
|---|---|---|---|
send-keys | 22~35µs | 10.5~12.5µs | 1.7~3.3× ⛔ |
capture-pane (바쁜 세션) | 40µs | 13~15µs | 2.6~3.0× ⛔ |
둘 다 프로세스 사이 왕복 한 번이 바닥이에요. tmux -C 대비 10배가 되려면 3.5µs 이하여야 하는데, 소켓 왕복 하나(문맥 전환 두 번)가 그보다 커요. 그래서 언어를 바꿔도 안 되고, 왕복을 없애는 임베디드 모드만 10배를 넘어요.
- 위 표는 요청을 쉬지 않고 연달아 보낸 값이에요. 실제로는 요청 사이에 쉬는 시간이 있어요. 그래서 요청 전에 쉬는 간격을 두고 다시 쟀어요(
latency_c ... 300 <간격 µs>, 2026-10-19, 바쁘지 않은cat세션이 아니라 claude 재생 세션). 쉬었다 보내면 둘 다 느려지고 배수는 1.7배로 줄어요.
| C 클라이언트, p50 | tmux -C | zeropty 소켓 | 배수 |
|---|---|---|---|
send-keys, 연달아 | 26.1µs | 9.3µs | 2.8× ⛔ |
send-keys, 1ms 쉬고 | 65.5µs | 36.7µs | 1.7× ⛔ |
send-keys, 25ms 쉬고 | 99.2µs | 56.4µs | 1.7× ⛔ |
capture-pane, 25ms 쉬고 | 108.2µs | 62.3µs | 1.7× ⛔ |
- 서버 플러그인(임베디드 호스트가 연 소켓)도 독립 서버와 같은 속도예요.
bench/listen_host.c(polled 엔진이 소켓을 엶)와zeropty serve를 같은 C 클라이언트로 번갈아 2회 쟀어요(2026-10-19). 이날은 기기가 더 느린 상태여서 tmux -C도 위 표의 2배쯤 나왔고, 배수는 비슷해요.
| C 클라이언트, p50 | tmux -C | 독립 서버 | 서버 플러그인 |
|---|---|---|---|
send-keys, 연달아 | 26.6~44.6µs | 12.0~20.4µs | 12.4~22.2µs |
capture-pane, 연달아 | 41.5~74.1µs | 14.6~27.3µs | 14.2~28.7µs |
send-keys, 25ms 쉬고 | 224~252µs | 120~124µs (1.8~2.1×) | 126~130µs (1.7~2.0×) |
capture-pane, 25ms 쉬고 | 297~360µs | 143~149µs (1.9~2.5×) | 148~160µs (1.8~2.4×) |
쉬었다 보낼 때 플러그인이 3~8% 느려요. 원인은 아직 확인하지 않았어요. polled 호스트는 엔진 fd를 poll로 기다린 뒤 zeropty_process에서 kqueue를 한 번 더 읽어서, 시스템 호출이 하나 더 있어요.
- 쉬었다 보낼 때의 바닥은 "통로"가 아니라 잠든 코어를 깨우는 비용이에요(2026-10-19, C로 직접 잼). 두 프로세스가 1바이트를 주고받는 왕복을 방식별로 쟀어요. 연달아 보내면 방식마다 0.7~10µs로 다르지만, 1ms만 쉬어도 모두 12~21µs, 25ms 쉬면 28~34µs가 돼요. 코어를 계속 돌리는 spin만 0.08µs인데, 코어 하나를 100% 써요.
| 왕복 방식 (p50) | 연달아 | 1ms 쉬고 | 25ms 쉬고 |
|---|---|---|---|
| socketpair (zeropty가 쓰는 방식) | 9.7µs | 17.6µs | 33.6µs |
| pipe | 5.6µs | 12.2µs | 28.2µs |
공유 메모리 + __ulock 대기 | 6.5µs | 20.5µs | 28.2µs |
| 공유 메모리 + 200µs spin 후 대기 | 0.7µs | 13.3µs | 28.3µs |
mach_eventlink (커널이 상대 스레드로 바로 넘김) | 2.1µs | 14.0µs | – |
| 공유 메모리 + 계속 spin | 0.08µs | 0.08µs | (코어 100%) |
tmux -C가 25ms 쉬고 99µs이니 10배는 9.9µs인데, 이 바닥(28~34µs)보다 작아요. 그래서 쉬었다 보내는 조건의 10배는 별도 프로세스로는 불가능해요.
- 서버 안의 처리 시간도 식어서 늘어요. 25ms 쉬고 보낸
has-session에서 서버가 요청을 받아 답을 쓰기까지 18.5µs였어요(연달아 보내면 2.4µs). 소켓 읽기, JSON 해석, 명령 실행, 답 쓰기가 골고루 6~7배 느려졌어요. 특정 코드가 아니라 식은 CPU와 캐시 때문이에요. - 시도했지만 넣지 않은 것 (2026-10-19, 모두 번갈아 측정):
- 서버 스레드 QoS를 USER_INTERACTIVE로: 37.5~39.9µs vs 39.1~39.2µs, 차이 없음.
- LTO +
codegen-units=1(코드를 모아 식은 상태에서 덜 건드리게): 1~2µs(3%), 잡음 수준. - kqueue가 알려 준 바이트 수만큼만 읽어서 "더 없음" 확인 읽기를 생략: 차이 없음.
- 답한 뒤 서버가 200µs 동안 다음 요청을 기다리며 spin: 연달아 보내는 구간의
send-keys가 8.0µs에서 4.5µs(tmux -C 대비 5.8배)가 되지만, 쉬었다 보내면 그대로예요. 대신 요청마다 코어를 200µs씩 붙잡아 CPU 목표를 깎아요. 공유 메모리로 바꾸면 계산상 1.5~2µs까지 가지만 역시 연속 구간에서만이라, µs 몇 개는 사람이나 claude의 속도에서 체감되지 않아요. mach_eventlink: 스레드 하나가 링크 하나에서만 기다리는 방식이라 kqueue 하나로 모든 연결을 보는 서버에 넣으려면 연결마다 스레드가 필요해요(스레드마다 할당기 힙, 앞의 GCD 시험에서 +0.8MB). kqueue와 함께 쓸 수 있는 Mach 메시지는 소켓보다 2µs 빠를 뿐이에요.
- 클라이언트에 화면 미러를 두는 방식(mosh, tmux
%output처럼)은 넣지 않았어요(2026-10-19). 클라이언트가attach -r로 출력을 받아 화면을 직접 해석하면 capture에 왕복이 없어져요. 하지만 바쁜 세션 200개를 모두 미러링하자 서버 CPU가 2.50%에서 6.15%가 됐어요. 누가 보는 세션은 300ms씩 모아 읽지 않고 바로 읽어서 보내기 때문이에요. 클라이언트도 초당 573KB를 다시 해석해야 해요. 그리고 미러의 capture는 서버 capture와 달리 쌓인 출력을 확인하지 않아서, 빠른 만큼 오래된 화면일 수 있어요. 모아 읽기를 유지하면 CPU는 그대로지만 화면이 최대 300ms 늦어요. 화면을 계속 받는 용도는attach -r, 변화를 기다리는 용도는wait-pane이 이미 해요. - 동기 클라이언트(
zeroptySync,zeropty.node의Client)는 회차별 3.0~4.1배예요(⛔ †, p50 0.13~0.26ms, 같은 세 번). 이벤트 루프를 거치지 않고 블로킹으로 쓰고 읽어서 p50이 비동기보다 약 25~48% 낮아요. 남은 하한은 서버와 Node 두 프로세스가 번갈아 깨어나는 왕복이에요. 서버가 바쁠 때는 p95가 1ms까지 튀어요. - 임베디드는 그 하한이 없어요. 남은 하한은 Node 스레드가 잠들었다 깨는 시간(~40~50µs)이에요. 연달아 부르면 한 번에 2.6µs예요.
응답 속도: 셸에서 (bench/shell.zsh)
셸 스크립트에서 명령 200번씩, 바퀴마다 번갈아 쟀어요. bench/shell.zsh를 세 번, 한 번에 두 바퀴씩 모두 여섯 바퀴예요(2026-10-21, Mac mini M1). 단위 ms(p50 / p95)이고, 범위는 여섯 바퀴의 값이에요.
| 명령 | tmux | zeropty 프로그램 | zeropty zsh 함수 | zsh 함수 vs tmux |
|---|---|---|---|---|
has-session | 6.04~6.19 / 6.74~7.20 | 3.59~3.95 / 4.22~4.85 | 0.11~0.17 / 0.14~0.38 | 35.5~56.2× |
send-keys -l | 6.11~6.33 / 6.92~9.86 | 3.68~3.92 / 4.25~5.39 | 0.12 / 0.15~0.20 | 50.9~52.7× |
capture-pane -p -S -2000 | 6.67~6.75 / 7.41~7.76 | 3.86~4.23 / 4.47~5.52 | 0.50~0.55 / 0.57~1.13 | 12.1~13.5× |
- 두 프로그램 모두 2026-10-18보다 오래 걸렸어요(tmux 4.35~5.02ms → 6.04~6.75ms, zeropty 프로그램 2.47~2.60ms → 3.59~4.23ms). 기기가 더 바빴어요. zsh 함수는 거의 그대로라(0.11~0.48ms → 0.11~0.55ms) 배수가 올라갔어요.
- 프로그램끼리는 1.5~1.7배예요. 명령마다 프로세스를 띄우는 비용(빈 C 프로그램도 1.3ms)이 하한이에요.
- zsh 함수(
shell/zeropty.zsh)는 그 하한을 피해요. zsh에 기본으로 들어 있는 소켓 모듈(zsh/net/socket)로 셸 안에서 서버에 바로 말해서, 프로세스를 띄우지 않아요. - bash는 builtin(
shell/zeropty_bash.c→dist/zeropty_bash.dylib)으로 같은 일을 해요. bash 3.2에는 µs 시계가 없어서 200번 도는 루프 전체 시간에서 빈 루프 시간을 뺀 평균이에요(같은 스크립트 끝부분).
| 명령 (bash, 평균, 여섯 바퀴) | tmux | zeropty 프로그램 | zeropty bash builtin | builtin vs tmux |
|---|---|---|---|---|
has-session | 5.968~6.470ms | 3.630~3.860ms | 0.041~0.075ms | 79.5~157× |
send-keys -l | 6.022~6.313ms | – | 0.043~0.048ms | 125~146× |
capture-pane -p -S -2000 | 6.733~7.066ms | – | 0.114~0.138ms | 48.7~61.9× |
위와 같은 여섯 바퀴예요. builtin has-session이 한 바퀴에서 2.448ms가 나왔고 나머지 다섯 바퀴는 0.041~0.075ms였어요. 그 바퀴의 다른 명령은 정상이라 기기가 잠깐 멈춘 것으로 보고(확인하지는 않았어요) 범위에서 뺐어요. 넣으면 아래 끝이 2.4×예요.
대량 출력
프로그램이 5.4MB(색상 포함 6만 줄)를 최대한 빨리 출력해요(bench/bulk.sh, 2026-10-21, Mac mini M1, tmux와 번갈아 3번씩, 두 번).
| 항목 | tmux | zeropty | 배수 |
|---|---|---|---|
| 프로그램이 출력을 끝내기까지 | 166~176ms | 3.0~4.6ms | 36.0~58.6× |
프로그램이 출력을 시작해서 마지막 줄이 capture-pane에 보이기까지 | 169~188ms | 15~16ms | 10.5~12.5× |
화면 시간이 10배를 넘은 건 tmux가 느려졌기 때문이에요. 2026-10-18에는 159~164ms 대 zeropty 14~16ms(9.9~11.7×, 경계)였어요. zeropty는 두 날 모두 같은 시간이 걸렸어요.
출력이 파이프를 채우면 zeropty는 프로그램이 멈출 때까지 전부 먼저 읽고, 파싱은 그다음에 해요. 그래서 프로그램이 파싱을 기다리지 않아요. 이전에는 16KB씩 읽고 파싱하는 동안 프로그램이 막혀서 17ms였어요. 하한은 같은 프로그램이
cat > /dev/null에 쓸 때의 1.6ms예요.화면 쪽은 스크롤되어 나가는 행의 압축을 한 번의 쓰기가 끝날 때까지 미뤄요. 6만 줄 중 스크롤백(2000줄)에 남는 행만 압축해요. 5.4MB 파싱이 16.5ms에서 약 9ms가 됐어요(
cargo run --release --example parse_bench bulk).화면 시간은
capture-pane을 쉬지 않고 불러서 재요. 매번 프로세스를 띄워서(zeropty ~2.5ms, tmux ~4.5ms) 그만큼 오차가 있어요. 이전 측정(31~41ms)에는new-session과 시각을 재는 perl 실행 비용이 더해져 있었어요.
기다리기: 폴링 대신 wait-pane (node bench/wait.mjs 200)
호스트가 "세션이 일을 끝냈는지"를 알아내는 두 방식을 비교했어요. 세션 200개가 실제 claude 작업(tests/streams/claude2.trace)을 5~25초 재생한 뒤 TASK-DONE을 출력해요.
- 폴링: 호스트가 남은 세션을 P ms마다
capture-pane으로 읽어요. tmux 호스트는 이 방법밖에 없어요. wait-pane: 세션마다wait-pane -m TASK-DONE을 한 번 보내고 답을 기다려요.
2026-10-21, Mac mini M1에서 두 바퀴 번갈아 측정했어요. "감지 지연"은 줄이 출력된 순간부터 호스트가 알아챈 순간까지예요.
| 방식 | 서버 CPU | 호스트(Node) CPU | 감지 지연 p50 / p95 |
|---|---|---|---|
| 500ms 폴링 | 2.0% | 0.6% | 253~262 / 479~489ms |
| 200ms 폴링 | 2.9~3.0% | 1.1~1.3% | 86~96 / 173~196ms |
wait-pane | 3.8~3.9% | 0.3~0.4% | 1 / 2ms |
- 알아채는 시간이 86~262배 짧아요(p50 86~262ms → 1ms). "프롬프트가 뜨면 다음 입력 보내기"를 반복하는 자동화는 단계마다 그만큼 빨라져요. 호스트에서 폴링 코드도 사라져요.
- CPU는 공짜가 아니에요. 서버 CPU가 200ms 폴링보다 0.8~1.0%p 늘어요(세션 200개). 기다리는 세션은 출력을 300ms씩 모으지 않고 바로 읽기 때문이에요. 같은 세션을 모으기 없이 읽기만 해도 서버 CPU가 2.0%에서 2.8%로 늘었어요. 대기가 끝나면 원래대로 돌아가요.
- 글자 검사는 이번 읽기가 바꾼 행만 해요. 처음 만든 판은 읽을 때마다 화면 전체를 글자로 바꿔 검사해서 서버 CPU가 4.4~6.3%였어요. 바뀐 행만 보게 해서 2.7~3.2%가 됐고, 남은 비용은 바로 읽는 비용이에요. 화면 전환(대체 화면에서 나올 때)은 예전 행이 다시 보이므로 전체를 검사해요.
- 출력을 모아 읽는 그대로 기다리게 해 봤더니 서버 CPU는 폴링보다 15% 적었지만, 감지 지연이 p50 340ms로 500ms 폴링보다 늦었어요. 그래서 바로 읽는 방식을 택했어요.
여러 세션에서 글자 찾기: grep-panes (bench/grep.zsh 1000)
대기 중인 claude 화면 1000개 중 3개에 표시를 넣고, 그 3개를 찾는 데 걸린 시간이에요(2026-10-21, Mac mini M1, bench/grep.zsh 두 번, 한 번에 두 바퀴씩).
| 방법 | 시간 | 서버 메모리 (전 1.41MB) |
|---|---|---|
CLI로 세션마다 capture-pane | 4.2~4.3초 | 1.70~2.05MB |
zsh 함수로 세션마다 capture-pane | 0.88~1.02초 | 1.71~1.81MB |
Node(client/zeropty.mjs)로 세션마다 capture-pane | 92~118ms | 2.11~2.48MB |
grep-panes | 16~32ms | 1.60~1.76MB |
- 터미널에서 "어느 세션이 입력을 기다리지?"가 약 4초에서 16~32ms가 돼요.
- 방법마다 잰 메모리에는 앞 바퀴들이 펼쳐 둔 화면이 들어 있어요.
grep-panes는 바퀴마다 맨 먼저 돌고, 첫 바퀴 값은 1.60~1.63MB예요. - 화면을 녹이지 않아요.
capture-pane은 읽은 화면을 다시 얼 때까지 펼쳐 두지만(1000개면 +1MB 남짓),grep-panes는 언 화면의 사본을 읽어요. - tmux에는 화면 내용을 찾는 명령이 없어요(
find-window -C는 대화형 선택 화면이에요).
10배 목표 현황 (전체)
| 항목 | 상태 |
|---|---|
| CPU: 200·400개의 모든 부하 (화면 갱신, claude 기록 통째 출력, 실제 claude 작업) | ✅ 13.7~17.9× |
| 메모리: 화면 갱신, 실제 claude 작업 200·400개 (독립 서버) | ✅ 10.4~147× |
| 메모리: 임베디드 대기 400개, 실제 작업 200·400개 | ✅ 13.1~19.9× (polled), 10.4~14.4× (엔진 스레드, 대기 400개 10.8×) |
| 메모리: 세션 하나당 증가분 | ✅ 대기·화면 갱신·실제 작업 14.1~1207×. ⛔ claude 기록 통째 출력 7.9~28.9× (두 회차가 352KB 차이, 독립 서버 절 참고) |
| 응답 속도: 명령마다 tmux 실행 대비 | ✅ 소켓 31.0~72.1×, 임베디드 54.7~280× (회차별 57~268× †) |
| 응답 속도: tmux 제어 모드 대비, 임베디드 | ⛔ 키→화면 8.9~10.8× (회차별 9~10× †), send-keys 6.0~8.1× (cooked 세션의 에코), capture 3.2~10.0× (원인은 위 응답 속도 절). 범위의 아래 끝이 10배 미만이라 README에는 올리지 않아요 |
| 응답 속도: tmux 제어 모드 대비, 소켓 | ⛔ 1.6~2.8× (비동기, 회차별 1.7~2.8× †), 3.0~4.1× (동기 클라이언트, †), 프로세스 간 왕복 하한 |
| 메모리 절대값: 독립 서버의 대기, claude 기록 통째 출력 | ⛔ 4.1~6.9×, 서버 프로세스 하한 (빈 C 프로세스 0.87MB, 아래 메모리 절) (임베디드 polled로 넘음: 대기 12.8~17.7×, 실제 작업 13.1~19.9×. 서버 플러그인을 켜서 CLI·attach로 다뤄도 대기 200개 12.8~13.1×) |
| 메모리: 임베디드 대기 200개 | ✅ 12.8~13.0× (polled, Node 기본값). 엔진 스레드 방식은 ⛔ 7.2~7.5×, 스레드 비용 308~309KB |
| 대량 출력: 프로그램이 출력을 끝내기까지 | ✅ 36.0~58.6× |
| 대량 출력: 화면에 보이기까지 | ✅ 10.5~12.5× (2026-10-18에는 경계 9.9~11.7×. 이번엔 tmux가 느렸고 zeropty는 같았어요. 화면 시간에는 capture-pane을 프로세스로 띄우는 오차가 들어 있어요, 위 대량 출력 절) |
| 응답 속도: 셸에서, zsh 함수 vs tmux CLI | ✅ 12.1~56.2× |
| 응답 속도: 셸에서, bash builtin vs tmux CLI | ✅ 48.7~157× (프로그램끼리는 1.5~1.7×, 프로세스 실행 비용 하한) |
측정 다시 하기
./build.sh
bash bench/suite.sh # 독립 서버 메모리 · CPU, 약 15분
clang -O2 -o /tmp/embed_host bench/embed_host.c -Ldist -lzeropty_c -Wl,-rpath,$PWD/dist
/tmp/embed_host 400 trace $PWD # 임베디드 메모리 (idle | trace)
/tmp/embed_host 200 idle $PWD polled ~/.zeropty/eh.sock # 서버 플러그인을 켠 임베디드 메모리
clang -O2 -o /tmp/listen_host bench/listen_host.c -Ldist -lzeropty_c -Wl,-rpath,$PWD/dist
/tmp/listen_host ~/.zeropty/lh.sock $PWD & # 서버 플러그인 응답 속도: latency_c ~/.zeropty/lh.sock <tmux -L 이름> 400 [간격 µs]
node bench/latency.mjs 200 # 응답 속도
bench/bulk.sh zeropty; bench/bulk.sh tmux # 대량 출력
bench/shell.zsh # 셸에서 응답 속도 (zsh 함수, 프로그램, tmux)
node bench/wait.mjs 200 # 폴링 대 wait-pane (CPU, 감지 지연)
bench/grep.zsh 1000 # 세션 1000개에서 글자 찾기 (grep-panes 대 capture 반복)세션 1000개 측정은 사용자당 프로세스 한도(kern.maxprocperuid, 기본 2666)를 확인한 뒤 진행해요. load.sh가 미리 확인하고 넘으면 멈춰요.