성능 기준
이 문서가 이 라이브러리의 성능 규범이다 — 무엇을 재고, 무엇에 견주고, 언제 행동하는가. 결정의 역사는 여기 없다: 왜 지금 최적화하지 않는가, 구현했다 되돌린 후보 셋의 값과 대가는 ADR-0011이 갖는다. 이 문서는 기준과 대장을 갖고, 측정이 쌓이면 대장이 자란다.
수립 경위: senior-review-2026-08-11.
예산 — 이중선
| 선 | 값 | 어기면 |
|---|---|---|
| 60Hz 바닥 | 프레임 16.7ms | 결함이다. 원인을 격리(짝 비교)하고 고친다 |
| 120Hz 감시선 | 프레임 8.3ms | ADR-0011 재개봉을 검토한다 — 보류된 후보들의 값과 대가가 거기 있다 |
(1000/60 = 16.7, 1000/120 = 8.3. 문서들에 떠돌던 16.6ms는 같은 예산의 다른 반올림이었고, 이 문서를 세우며 전부 16.7로 통일했다.)
판정은 절대값이 아니라 여유 배수로 한다 — 같은 코드도 세션이 다르면 절대값이 20~25% 흔들리기 때문이다(ADR-0011).
여유 배수 = 예산 ÷ 그 세션의 중앙값
| 여유 | 판정 |
|---|---|
| ≥ 3× | 여유. 아무것도 하지 않는다 |
| 1.25~3× | 주시. 다음 대장 행에서 같은 시나리오를 다시 본다 |
| < 1.25× | 행동. 같은 세션의 짝 비교로 원인을 격리한다 |
CI에는 걸지 않는다 (계획 3.6, 2026-08-09 기각): 세션 간 20~25% 편차에 공유 러너는 더하다 — 임계가 느슨하면 회귀를 놓치고 빡빡하면 거짓 경보다. 결정적 회귀 방벽은 따로 있다: 명령 테스트의 명령점 수, 번들 예산, 그리고 이 문서의 시나리오 이름 대조다.
기준 구성
기계가 지키는 것 / 사람이 재는 것
프레임 시간은 CI가 못 지킨다 — 같은 코드도 세션 간 20~25% 흔들린다(계획서 :1751의 기각, 근거 유효). 대신 일의 양은 결정적이라 셀 수 있다.
| 층 | 지키는 것 | 어디에 |
|---|---|---|
| 일의 양 | "안 해도 되는 일을 안 한다" — 같은 봉의 틱은 x 인덱스를 다시 안 세고, 팬은 데이터 경로를 안 지나고, 같은 프레임은 다시 안 솎는다 | packages/core/src/__tests__/perf-invariants.test.ts (CI) |
| 이름 | 이 문서의 시나리오가 벤치에 실존한다 | 이름 대조 (CI) |
| 시간 | 프레임·p95·commit% | bench.sh 짝 비교 — 사람이 잰다 |
세는 층을 넓히는 법: 최적화가 *"빠르게 한다"*면 못 세지만, **"빠른 길과 느린 길 중에 고른다"**면 셀 수 있다 — 어느 길로 갔는지는 시간과 달리 안 흔들린다. 그때 양쪽을 다 막는다: 빠른 길이 죽으면 되돌아감이 늘고(상한), 갈림 자체가 사라지면 되돌아감이 0이 된다(하한). 하한이 없으면 최적화를 통째로 되돌릴 때 계수기도 같이 사라져 조용히 통과한다 (XMappingProbe가 그 첫 사례다). 그리고 비율 상한만으로는 부족하다 — 판을 점마다 여는 회귀는 분자·분모를 같이 부풀려 비율을 통과한다. 판 수 자체의 절대 상한을 같이 건다(변이 테스트로 실제로 새는 것을 확인했다).
두 층은 서로를 대신하지 않는다. 실측이 그것을 양쪽으로 보였다: ④는 호출 수로는 0/1이 뚜렷한데 프레임 시간으로는 노이즈에 묻혔고, 반대로 커서 훑기·힌트 탐색은 *"몇 번 하는가"*가 그대로라 일의 양으로는 안 잡히고 시간으로만 보였다.
기준이 되는 시나리오들이다. 이름은 apps/examples/src/bench.ts의 시나리오 이름 그대로다 — 어긋나면 perf-doc-check가 CI에서 잡는다.
⚠️ 좌표계를 적는다. 좌표계가 안 적힌 시나리오는 전부 연속 x(코어 기본값)다. 그런데
presets.ts가 *"금융 차트는barIndexX를 넣는다"*고 가르치고 우리 트레이딩 화면 둘 다 그렇게 배선한다 — 즉 이 문서가 오래 *"실전 구성"*이라 부른 수치가 우리가 파는 좌표계를 한 번도 안 지났다. 이제 짝으로 잰다. 갈리는 곳은 데이터 변경 경로다(봉 번호 매핑만rebuild를 갖는다).
| 시나리오 | 무엇의 대표인가 |
|---|---|
호버 · 100k 캔들 | 가장 흔한 프레임 — 커서만 움직인다 |
팬 · 100k 캔들 | x 도메인 변경 — 매 프레임 재절단·재솎기·눈금 재계산 |
줌 · 100k 캔들 | 도메인 폭까지 변경 |
틱 추가 · 100k 캔들 | 실시간 갱신 — 복사·계약 검사·재계산이 이력 크기와 만나는 곳 |
호버 · 100k, 지표 4개 | 파생 시리즈가 얹힌 실전 구성 |
팬 · 100k, 지표 4개 | 가장 무거운 기준 프레임 — 120Hz 감시선이 처음 닿는 곳 |
호버 · 100k, 지표4+드로잉+툴팁 (R3b) | 전체 표면 — 이벤트 경유 호버, probe·툴팁 DOM 갱신 포함 |
드래그 · 100k, 지표4+드로잉+툴팁 (R3b) | 입력 라우터를 실제로 지나는 첫 시나리오 — 소비자 스택·캡처·히트 판정·moveGrip |
드래그 · 드로잉 200 | 히트 판정(gripAt)이 도형 수에 비례하는 비용의 자리 |
틱 추가 · 100k, 지표 4개 (연속 x) | 실시간 갱신 × 파생 시리즈 — 두 축이 만나는 칸 |
틱 추가 · 100k, 지표 4개 (봉 번호) | 같은 구성의 봉 번호 좌표계 짝 |
호버 · 100k, 지표 4개 (봉 번호) | 호버의 봉 번호 짝 — 그리기 toPixel(이진 탐색)의 계기판 (Q31) |
과거 붙이기 · 100k 캔들 (+500/프레임) | 무한 스크롤 페이지 수신 — 히치 방아쇠(>8ms)의 계기판 |
과거 붙이기 · 100k 캔들 (봉 번호) | 같은 것의 봉 번호 짝 — prepend는 설계상 인덱스 전체 걸음이다 |
팬 · 100k, 지표 4개 (봉 번호) | 팬 · 100k, 지표 4개의 봉 번호 짝 |
틱 갱신 · 100k, 지표 4개 (연속 x) | 진행 중인 봉의 틱 — 실제 피드의 99%. 형제 틱 추가는 매 프레임 새 봉이라 다른 일이다 |
틱 갱신 · 100k, 지표 4개 (봉 번호) | 같은 구성의 봉 번호 짝 |
콜드 스타트(첫 그림까지, 아래 절):
| 시나리오 | 무엇의 대표인가 |
|---|---|
1k 캔들 | 최소 무대 |
100k 캔들 | 데이터만 큰 무대 |
100k 캔들 + 지표 4개 | 기준 콜드 스타트 — 첫 계산은 데시메이션이 못 막는다 |
벤치의 나머지 시나리오(오토스케일 켬/끔, maxPoints, 층, MACD 짝 등)는 탐사용이다 — 특정 질문에 답하려고 있고, 기준이 아니다.
콜드 스타트 예산
정의: 무대를 세워 첫 render()가 끝날 때까지. 데이터 준비는 제외한다 (uPlot 벤치의 done과 같은 정의).
예산: 기준 콜드 스타트(100k 캔들 + 지표 4개)가 60Hz 한 프레임 (16.7ms) 안. 임의의 이상값이 아니라 실측에 래칫한 값이다 — 번들 예산(계획 3.5)과 같은 정신. 지표의 첫 계산이 이 예산을 처음 위협했고, 계산 노드(ADR-0018)가 그 답이었다 — 다음 위협도 같은 종류일 것이다.
드롭 프레임 — 사용자 체감 지표
프레임 시간 중앙값은 개발자 지표다. 사용자가 느끼는 것은 "조작 중 프레임을 놓쳤는가"이고, 하네스의 rAF 실측(measureFrameRate)이 그걸 잰다. 기준: 기준 구성에서 드롭 프레임 0 (rAF 간격 p95가 디스플레이 주기의 1.5배 안).
번들 예산
성능 기준의 일부지만 이쪽은 결정적이라 CI가 직접 지킨다 — packages/core/.size-limit.json과 packages/dom/.size-limit.json (수치의 원본은 그 파일들이다, brotli). 실측에 래칫했고, 넘기려면 예산을 고치는 커밋이 함께 가야 한다 — 그 커밋 메시지는 증가분이 어디서 왔는지 수치로 적는다 (전례: 2026-08-14 extensions 승격, 순수 이동의 모듈 순서 변화 +20 B).
측정 프로토콜
수치를 만들 때의 규칙이다. 어기면 수치가 거짓말한다 (ADR-0011에서 비싸게 배웠다).
- 빌드 먼저.
npx turbo run build— dev 서버 경로는^build의존이 없어 옛 코드를 조용히 잰다. - 짝 비교만 세션을 넘어 믿는다. 같은 세션에서 블록 단위로 번갈아 돌린 비율(
measurePair)이라야 환경 편차가 상쇄된다. 노이즈 바닥은 ±4% — 2% 차이를 이 하네스로 가르려 하지 말 것. - 절대값·콜드 스타트는 같은 세션 안에서만 나란히 둔다. 세션이 다르면 20~25% 흔들린다. 대장의 행끼리 절대값을 비교하지 않는다 — 보는 것은 각 행의 여유 배수다.
- DPR을 명기한다. agent-browser 경로(
bench.sh)는 DPR 1, 손 Chrome은 DPR 2 — commit 몫이 특히 달라진다. - 할당은 하한이다. CDP 증가분 합산이라 GC가 이미 회수한 몫이 빠진다. 힙 샘플러의 함수 귀속은 믿지 않는다(ADR-0011의 틀린 가설).
visiblePoints(발행된 명령의 점 수)는 결정적 회귀 방벽이다. 구조 변경 전후로 이 열이 그대로면 그리는 양이 그대로다.
측정 실무는 perf-measurer 에이전트가 표준 절차로 갖고 있다.
이 기준이 재지 않는 것
기준은 자기 한계를 스스로 말해야 한다.
- 래스터·합성. 측정 범위는 메인 스레드뿐이다 — dpr을 올려도 숫자가 안 움직이는 것이 그 증거(ADR-0011). 그쪽이 궁금하면 트레이싱이 필요하다.
- 작도 드래프트·끝점 드래그 프레임. 비용 구조가 팬(전체 리드로 + 지오메트리 하나)과 이벤트 경유 호버에 상한이 잡혀 있다고 보고 보류. 작도·드래그 중 버벅임 실보고가 오면 시나리오를 더한다 — 하네스 구조상 배열 항목 하나다.
- 히트 판정의 클릭 지연. 클릭당 O(드로잉 수). 드로잉 수백 개 실수요가 오면 잰다.
- 리사이즈 스톰, 프레임당 다중 틱, 메모리 총량. 실수요가 정의를 가져올 때까지 재지 않는다.
측정 대장
행마다 날짜·커밋·세션(DPR 포함)을 명기한다. 행끼리 절대값을 비교하지 말 것 — 각 행 안의 여유 배수를 본다.
2026-08-23 · C족 실측 스위프 — 히치 계기판 신설, 방아쇠 전부 불발
원장의 실측-방아쇠 항목들을 한 번에 쟀다. 프레임 히치(방아쇠: prepend 프레임 >8ms)는 재는 자리가 없어 계기판부터 신설했다(+500봉/프레임 — 무한 스크롤 페이지 모양): 연속 x mean 1.58 · max 2.70ms, 봉 번호 mean 3.20 · max 4.80ms — 봉 번호 prepend가 인덱스 전체 걸음인데도 방아쇠의 반값이다. 불발, 계기판 상주. R3b(호버 3.8~3.9ms — 예산 1/4)· getComputedStyle(전 시나리오 예산 안)도 오늘 수치로 불발 재확인.
2026-08-23 · Q33 — 솎기가 아는 자리를 나르니 봉 번호 웃돈이 사라졌다
Q31이 가른 진실("몫 2ms는 점당 병합 xs 참조 그 자체")의 처방을 사용자 지시로 열었다. 자리 계산을 매니저의 뷰포트 캐시 층으로 올린다: DataManager.getVisiblePlaced가 점과 나란한 자리 배열을 캐시에 채우고 (Viewport.xEpoch가 재빌드 낡음을 버리는 열쇠), 시리즈 여섯은 screenXAt 한 문으로 domainToPixel(place) — 점당 산술 하나다. 호버처럼 뷰포트가 안 변하는 프레임은 점당 탐색이 통째로 사라진다.
| 호버 · 100k, 지표 4 | 프레임 |
|---|---|
| 연속 x | 3.80 ms |
| 봉 번호 (Q33 전) | 5.00~5.20 ms |
| 봉 번호 (Q33 후) | 3.90 ms |
팬 11.50(직전 11.14 — 뷰포트가 매 프레임 변해 중립) · 틱 17.9/18.2(직전 19.1/18.7 — 회귀 없음). 가드: placePixels 하한 + pixelPoints 상한 + 낡음 끝-끝(자리 문 뗀 무대가 정본) — 변이 셋(epoch 정지·동반 차단·epoch 무시) 전부 빨간 것 확인.
2026-08-23 · Q31 구현 — 그리기 판은 통일됐고, 2ms의 정체가 밝혀졌다
사용자 지시(로직 일관성)로 보류를 열어 그리기를 훑기 판으로 통일했다: XMapping.scanToPixel 문 + pane이 등록마다 판을 연다(시리즈 저자는 같은 x.toPixel을 쓴다 — 컨텍스트가 여는 문). 세는 가드가 곧바로 실물 둘을 잡았다 — M4 버킷 안 순서 섞임(창 안 뒷걸음으로 처방), 솎기 보폭이 창(32칸)을 넘는 앞 점프(갤럽 + 보폭 예측으로 처방). slotWidth도 이웃 두 번 묻기에서 점당 한 번의 단조 훑기로 고쳤다.
시간은 중립이다: 호버 봉 번호 5.00(기준 5.20, 소음 안) · 팬 봉 번호 11.14(직전 ~11.4). 판별 변이(scan 내용만 항등 → 3.20)가 가른 진실: 몫 ~2.0ms는 탐색 알고리즘이 아니라 점당 병합 xs 참조 그 자체다 — 이진 17번을 예측 2번으로 바꿔도 안 떨어진다. 회수는 솎기가 이미 계산한 자리를 나르는 계약(원장 Q33)이고, 예산 안이라 보류다.
2026-08-23 · Q31 실측 — 그리기 탐색은 2ms, 예산 안이라 보류 유지
호버의 봉 번호 짝이 벤치에 없었다(방아쇠가 "호버 실측"인데 계기판이 없던 누락). 신설하고 귀속: 호버 봉 번호 5.20ms(연속 4.00), 그리기 toPixel 탐색을 우회하면 3.20ms — 몫 ~2.0ms/프레임. 추정(sub-ms)보다 컸지만 예산 깊숙이 안이라 방아쇠 불발, 보류 유지. 재개봉 조건이 숫자와 계기판을 다 갖게 됐다.
2026-08-23 · 병합 걸음 증분화 — 새 봉이 바닥에 붙었다
재빌드의 전체 걸음(O(N) 병합)이 새 봉마다 돌았다 — 귀속 14.5ms(33.20 ↔ 18.70, 같은 세션). entry의 xs 캐시를 제자리 성장으로 바꿔 배열 신원이 꼬리를 통지하게 했고, 매핑의 꼬리 빠른 길이 그것을 O(1)로 판정한다:
| 틱 추가 · 100k, 지표 4 (봉 번호) | 프레임 |
|---|---|
| 전체 걸음 | 33.20 ms |
| 증분 | 19.10 ms |
| 재빌드-끔 바닥 | 18.70 ms |
대조군(연속 x) 12.90 ↔ 13.10. 가드: 새 봉에 전체 걸음 0 · setData에 ≥1.
2026-08-23 · 입력 경로 첫 계측 (Q26)
routeInput을 지나는 시나리오가 0이었다 — 호버조차 plot.crosshair 직행. 드래그 둘을 넣고 첫 수치: R3b 무대 드래그 3.80ms(같은 무대 호버와 동일 — 라우터·gripAt·moveGrip이 노이즈 안), 도형 200개 드래그 0.70ms. 이전 원장이 물어 온 *"도형 수백 개에서 하나를 끄는 비용"*의 답은 무시 가능이다.
2026-08-23 · ⒜′ 첫 판 — 파생 꼬리 문으로 틱이 예산 안에
deriveLast(꼬리 증분 문) + smaFold(접기 하나에서 두 형태) + 벤치 MA 배선. 같은 세션 교대 짝:
| 틱 갱신 · 100k, 지표 4 | 문 끔 | 문 켬 |
|---|---|---|
| 연속 x | 22.80 ms | 14.70 / 13.20 ms |
| 봉 번호 | 28.70 ms | 19.00 / 18.70 ms |
연속 x가 60Hz 예산(16.7ms) 안에 처음 들어왔다. 이 문서의 틱 예산 위반 추적이 여기서 닫힌다 — 남은 봉 번호 ~2ms 초과는 새 봉의 병합 걸음(O(N))이고 tick-budget approach 4번의 몫이다. 틱 추가(새 봉)도 연속 12.90 · 봉 번호 33.30으로 내려왔다(직전 전수: 19.50 · 42.10).
이득의 근원은 계산이 아니라 경로다: 파생 시리즈가 틱마다 타던 setData 전체 재적재(재파생 O(n)+전수 검증+전수 x 대조+재사상)가 매니저 꼬리 연산 (O(count))으로 접혔다 → resumable-fold-approach-2026-08-23.md.
2026-08-22(리팩터 후 전수) · ca038ce · agent-browser(bench.sh), DPR 1, 1200×600
구조 변경(판 소유 커서 · 이력 단일 소유 · 갭 융합 · 꼬리 사상) 뒤 전 종목 24개 한 세션. 안 건드린 경로는 전부 기존 범위 안이다 — 호버 100k 0.80 · 팬 100k 1.10 · 호버 지표4 3.80 · R3b 4.10 · 스파크라인·층·오토스케일 불변. 봉 번호의 웃돈(연속 x 대비): 팬 +5.4ms(12.0−6.6) · 틱 갱신 +4.2ms(26.2−22.0) · 새 봉 ×2.2(42.1/19.5 — 재빌드-끔 바닥 ×2.4보다 낮다: 바닥 측정 당시엔 꼬리 사상이 없었다).
2026-08-22 · 1d02da0 · agent-browser(bench.sh), DPR 1, 1200×600
비어 있던 칸을 처음 쟀다. 실시간 갱신 × 파생 시리즈가 만나는 자리, 그리고 그 자리의 봉 번호 좌표계 짝이다. 둘 다 예산 밖이다.
| 시나리오 | 명령 점 | 중앙값 | p95 | commit% | 여유(60Hz) |
|---|---|---|---|---|---|
| 틱 추가 · 100k, 지표 4개 (연속 x) | 33,022 | 18.70 ms | 25.50 | 12% | 0.9× |
| 틱 추가 · 100k, 지표 4개 (봉 번호) | 33,022 | 115.20 ms | 122.40 | 2% | 0.14× |
| 팬 · 100k, 지표 4개 (봉 번호) | 32,718 | 26.50 ms | 29.70 | 9% | 0.6× |
같은 세션의 연속 x 짝: 팬 6.90 · 호버 3.80.
commit%가 진단을 다 말한다. 봉 번호 틱은 프레임의 98%가 그리기 밖이다 — 같은 명령 수(33,022)를 그리는 호버가 3.80ms인데 틱이 115ms다. 그리는 일은 그대로고 데이터 경로가 100ms를 먹는다. 원인은 Plot.rebuildX가 틱마다 전 시리즈의 x를 통째로 다시 세우는 것이다(x-mapping.ts의 sources.flat().sort()) — 연속 x는 rebuild가 없어 그 일 자체를 건너뛰므로 ×6.2가 좌표계의 몫이다.
연속 x도 18.70ms로 이미 예산 밖이라는 것이 두 번째 사실이다. 틱과 지표가 만나는 칸을 아무도 안 재서 그동안 안 보였다.
그리고 "틱"이라 부르던 것이 새 봉이었다. 틱 추가는 매 프레임 봉을 하나씩 잇는다 — 실제 피드는 봉 하나가 열려 있는 동안 같은 봉을 여러 번 갱신하고 봉이 바뀌는 것은 분에 한 번이다. 99%인 그쪽을 아무도 안 쟀다.틱 갱신 짝을 그래서 만들었다(같은 세션, 스킵 켠 전수 실행): 연속 x 28.70ms · 봉 번호 108.10ms.
→ 앞선 수정이 x 재빌드를 건너뛰지만 파생 등록에서 안 걸렸다(보수적으로 "모른다"를 답했다). 그 갈래를 결과 대조로 바꾸고 나서 같은 시나리오가 109.10 → 42.50~50.40ms가 됐다(같은 세션 교대, 연속 x 대조군 노이즈 ~9ms). 여전히 예산 밖이다 — 남은 것은 재빌드가 아니라 파생 재계산 자체다. 지표 넷의 몫이 17ms(지표 0에서 1.90 → 넷에서 18.90, 하나당 ~4.3ms)이고 derive가 *"원본이 바뀔 때마다 통째로 다시 계산한다"*고 선언한 대로 도는 비용이다. 계약 변경 후보라 approach 문서를 열었다.
봉 번호의 좌표 변환이 점마다 이진 탐색이었다. 처음엔 매핑 안의 공유 커서(힌트)로 고쳤는데, 그 커서를 훑지 않는 소비자(그리기·히트 판정)가 짓밟아 절반만 일했다(되돌아감 ~3,200회/프레임). 근본 수정: toDomain은 순수로 되돌리고, 커서는 scanToDomain()이 연 **판(pass)**이 소유한다 — 솎기 훑기마다 판 하나, 판끼리는 구조적으로 간섭 불가. 실측(대조군 일치): 틱 갱신 봉 번호 33.50 → 24.70ms, 팬 11.70 → 11.40ms. 봉 번호의 웃돈이 14.5 → 5.8ms. 솎기가 창의 점마다 화면 자리를 묻는데(screenPlaceOf → toDomain) 그때마다 xs 전체를 뒤졌다 — 10만 점 창이면 프레임당 10만 × log 10만이다. 훑기는 오름차순이라 직전 자리에서 시작하게 했다(답은 같다):
| 시나리오 | 힌트 끔 | 힌트 켬 | 대조군(연속 x) |
|---|---|---|---|
| 팬 · 100k, 지표 4개 (봉 번호) | 26.20 ms | 11.70 ms | 6.80 ↔ 6.80 |
| 틱 갱신 · 100k, 지표 4개 (봉 번호) | 40.90 ms | 33.50 ms | 19.00 ↔ 18.90 |
팬이 60Hz 예산 안으로 들어왔다. screenXOf를 아예 우회한 바닥이 28.90ms (틱)이므로 12ms 중 7.4ms를 회수했다.
이력의 소유가 하나가 됐다. identity 등록이 매니저와 같은 이력을 두 벌 들어 틱마다 복사가 두 번 나가고 진실이 둘이었다 — DataManager.read()로 매니저가 유일한 주인이 되고 entry는 별칭을 받는다. 검사 루프와 갭 탐지도 한 걸음으로 융합했고(같은 배열을 두 번 걸었다), 새 봉의 x 사상은 꼬리만 늘린다(extendXs — getX O(n)이 O(k)로). 새 봉 프레임 66.80 → 47.20ms(대조군 일치), 재빌드-끔 바닥 44.60에 근접. 가드는 전부 세는 층이다: getX가 이력에 비례하지 않는다 · 틱 여럿 뒤의 새 봉도 그렇다.
새 봉의 재빌드는 알고리즘을 바꿔 절반으로 줄였다. sources.flat() .sort()가 이미 정렬된 조각들을 다시 줄 세우고 있었다(ADR-0008이 등록마다 정렬을 보장한다). 커서 훑기로 바꾼 짝 비교 — 대조군(연속 x)을 19.00 ↔ 19.20으로 맞춘 실행에서 115.90 → 66.80ms. 재빌드를 통째로 끈 바닥이 대조군 대비 2.4배인데 병합이 3.5배까지 왔다(정렬은 6.1배였다). 남은 것은 훑기 자체로, 꼬리 추가를 증분으로 아는 것이 다음 자리다.
2026-08-11 · 31fd69b · agent-browser(bench.sh), DPR 1, 1200×600
기준 수립 시점의 첫 행. 측정: perf-measurer, 한 세션, 빌드 후 실행.
| 시나리오 | 명령 점 | 중앙값 | p95 | commit% | 여유(60Hz) | 여유(120Hz) |
|---|---|---|---|---|---|---|
| 호버 · 100k 캔들 | 3,640 | 0.60 ms | 0.80 | 43% | 27.8× | 13.8× |
| 팬 · 100k 캔들 | 3,636 | 1.00 ms | 1.20 | 29% | 16.7× | 8.3× |
| 줌 · 100k 캔들 | 3,634 | 1.00 ms | 1.30 | 29% | 16.7× | 8.3× |
| 틱 추가 · 100k 캔들 | 3,636 | 1.20 ms | 1.40 | 24% | 13.9× | 6.9× |
| 호버 · 100k, 지표 4개 | 33,022 | 3.40 ms | 4.10 | 55% | 4.9× | 2.4× |
| 팬 · 100k, 지표 4개 | 32,714 | 6.90 ms | 7.80 | 28% | 2.4× | 1.2× |
| 호버 · 100k, 지표4+드로잉+툴팁 (R3b) | 33,047 | 3.40 ms | 4.10 | 58% | 4.9× | 2.4× |
콜드 스타트: 1k 캔들 0.70 ms · 100k 캔들 1.50 ms · 100k + 지표 4개 15.20 ms (p95 22.70) — 예산 안, 여유 1.10×.
rAF 실측(호버·팬·파생 2/px): 셋 다 60fps, interval p95 ≤ 17.5 ms — 드롭 프레임 0. 크로스헤어 바닥 0.0006 ms/frame. 짝 비교: 오토스케일 팬 +1.5%·호버 +3.1%(노이즈 바닥 안), MACD 계산 노드 −52.2%(C1 효과 재확인). visiblePoints는 ADR-0011 2026-08-08 표와 동일 — 그리는 양 변화 없음.
판정. 60Hz 바닥은 전 시나리오 여유. 두 곳이 주시 구간이다:
- 팬 · 100k, 지표 4개 — 120Hz 여유 1.2×. ADR-0011 재개봉 신호가 가리키던 바로 그 구성이다. 짝 비교상 오토스케일 몫은 노이즈 안이라 원인은 파생 시리즈의 명령량(33k점) 자체다. 아직 감시선 안이고 실사용 드롭 보고도 없으므로 재개봉하지 않는다 — 다음 행에서 다시 본다.
- 기준 콜드 스타트 — 여유 1.10×는 세션 편차(20~25%)보다 작다. 다음 행이 예산 밖으로 나오면 그때가 행동 시점이다(첫 계산은 데시메이션이 못 막는다 — C1 방식의 확장이 예비 답이다).
2026-08-11 · 0f4d892 · playwright CDP, DPR 2, 1400×900 — 할당
같은 날이지만 위 프레임 행과 다른 세션이고 DPR도 다르다 — 절대값을 나란히 두지 말 것. CDP Runtime.getHeapUsage 증가분 합(하한), 시나리오당 150프레임. 기준 시나리오만 싣는다 (전체 표는 측정 보고 원본에).
| 시나리오 | 할당/frame |
|---|---|
| 호버 · 100k 캔들 | 3,620 KB |
| 팬 · 100k 캔들 | 2,183 KB |
| 틱 추가 · 100k 캔들 | 3,023 KB |
| 호버 · 100k, 지표 4개 | 8,085 KB |
| 팬 · 100k, 지표 4개 | 7,776 KB |
| 호버 · 100k, 지표4+드로잉+툴팁 (R3b) | 7,068 KB |
판정. ADR-0011의 2026-08-09 "후보 3 회수" 표(같은 DPR 2)와 같은 자릿수다 — 팬·지표4 7,776 대 6,450, 팬·100k 2,183 대 1,506 KB/frame. 어느 쪽도 2배에 못 미치므로 회귀 신호 없음(세션이 달라 % 비교는 하지 않는다 — 할당은 프레임 시간보다 더 흔들린다). GC가 따라오는지는 할당이 아니라 rAF 드롭 프레임으로 판정하고, 그쪽은 위 행에서 0이었다.
2026-08-16 · c7add44 · agent-browser(bench.sh), DPR 1, 1200×600
08-11 행의 주시 2건 재판정 + 그 사이 대형 변경(ADR-0038·0039·0040·0041, swapSeries entry 사슬, pane 경계선)의 회귀 점검. 측정: perf-measurer, 같은 세션 2회 런(중앙값 편차 4% 이내), 빌드 후 실행.
| 시나리오 | 명령 점 | 중앙값 | p95 | commit% | 여유(60Hz) | 여유(120Hz) |
|---|---|---|---|---|---|---|
| 호버 · 100k 캔들 | 3,640 | 0.60 ms | 0.70~0.90 | 41~47% | 27.8× | 13.8× |
| 팬 · 100k 캔들 | 3,636 | 1.00 ms | 1.10~1.50 | 28~30% | 16.7× | 8.3× |
| 줌 · 100k 캔들 | 3,634 | 0.90~1.00 ms | 1.10~1.20 | 27~31% | ~17.6× | ~8.7× |
| 틱 추가 · 100k 캔들 | 3,636 | 1.20 ms | 1.40 | 23~25% | 13.9× | 6.9× |
| 호버 · 100k, 지표 4개 | 33,026 | 3.30~3.40 ms | 3.60~4.50 | 55~58% | ~5.0× | ~2.5× |
| 팬 · 100k, 지표 4개 | 32,718 | 6.60~6.70 ms | 7.40~7.90 | 27~29% | ~2.5× | 1.24~1.26× |
| 호버 · 100k, 지표4+드로잉+툴팁 (R3b) | 33,051 | 3.30~3.40 ms | 3.60~4.40 | 57~61% | ~5.0× | ~2.5× |
콜드 스타트: 1k 0.70~0.80 ms · 100k 1.50 ms · 100k + 지표 4개 13.90~14.90 ms (p95 22.60~25.70) — 예산 안, 여유 1.12~1.20×.
rAF 실측: 셋 다 60fps, 드롭 프레임 0. 크로스헤어 바닥 0.00055 ms/frame. 짝 비교: 오토스케일 팬 +1.6~+6.8%·호버 −0.0~+3.1%(노이즈 바닥 근처), MACD 계산 노드 −51.9~−52.4%(C1 효과 유지).
명령점 판정. 지표 없는 4개 시나리오는 08-11과 완전 동일 (3,634~3,640) — ADR-0040(x 좌표계 교체)도 swapSeries 사슬 교체도 그리는 양을 안 바꿨다. 지표4 계열 3개만 정확히 +4점(예: 32,714→32,718): pane 경계 캔버스 선이 pane마다 하나씩 새로 발행하는 것과 개수가 맞는다 — 회귀가 아니라 의도된 신규 그리기.
주시 재판정.
- 팬 · 100k, 지표 4개 — 1.24~1.26×, 08-11(1.2×)과 같은 자리. 짝 비교상 원인도 그대로(파생 명령량 32.7k점 자체). 재개봉 신호로 넘어가지 않았다 — 경계선에 계속 붙어 있으므로 다음 행에서 다시 본다.
- 기준 콜드 스타트 — 1.12~1.20×, 08-11(1.10×)과 같은 좁은 여유대. "다음 행이 예산 밖이면 행동" 기준은 발동하지 않았다.
판정. 이 기간의 대형 변경 어느 것도 프레임 시간·명령점 기준 회귀를 만들지 않았다. 60Hz 바닥은 전 시나리오 여유(최소 ~2.5×).
2026-08-17 · 0d65529 · agent-browser(bench.sh), DPR 1, 1200×600 — 인스턴스 수 축
이 대장이 그동안 안 재던 축이다. 위 모든 행의 무대는 "큰 차트 하나"라 비용이 화면 폭에 묶인다는 결론이 거기서 나왔다. 종목 리스트는 그 축이 아니라 인스턴스 수로 자란다. 무대는 sparkline 문서의 레시피 그대로다 — browserDeps가 아니라 createPlotDeps + 필수 셋, 행 120×32px. 같은 세션 3회 런.
| 시나리오 | 명령 점 | 중앙값 | p95 | commit% | 여유(60Hz) |
|---|---|---|---|---|---|
| 틱 · 스파크라인 50행 × 30점 | 1,500 | 0.20~0.30 ms | 0.30~0.40 | 36% | ~60× |
| 틱 · 스파크라인 50행 × 250점 | 12,500 | 1.00 ms | 1.20~1.40 | 20% | 16.7× |
| 틱 · 스파크라인 200행 × 30점 | 6,000 | 0.90 ms | 1.00~1.10 | 37% | 18.6× |
commit%열은 2026-08-21에 정정됐다 — 각각 12~19 · 15~17 · 18~19%로 적혀 있었다. 벤치의 계측 래퍼가Renderer.clip을 빠뜨려 재생 단계가 프로덕션과 다른 렌더러로 측정한 값이었다. 같은 세션에서 고친 하네스와 원래 하네스를 순차로 돌려 확인했다: 큰 차트 17종은 commitShare가 하나도 안 바뀌었고(그래서 위쪽 표들은 그대로다) 작은 캔버스인 스파크라인 3종만 같이 뛰었다. 중앙값·p95·명령 점은 세션 편차 안이라 손대지 않았다.
한 프레임에 모든 행이 틱을 받고 모든 행이 그려진다 — N행 전체 비용이 한 값에 담긴다.
콜드 스타트: 50행 1.10~1.20 ms · 200행 4.80~4.90 ms (p95 1.20~1.30 / 5.40~11.30).
읽어야 할 것 넷.
선형이고 상수가 작다. 인스턴스 하나의 한계 비용이 콜드 스타트 ~25 µs, 프레임 ~4 µs다. 200행 리스트가 서는 데 4.8ms인데, 같은 세션에서 지표 넷 얹은 차트 하나가 14.9ms다 — 리스트 전체가 큰 차트 하나의 1/3이다.
동시 틱도 예산의 5%다. 200행이 한 프레임에 전부 갱신되어도 0.90ms고, 같은 세션 호버·지표4(3.30ms)의 **27%**다.
commit 비중이 여전히 낮다 — 큰 차트는 호버에서 55~58%인데 리스트는 20~37%다. 작은 캔버스 N개에서는 비용이 재생이 아니라 서술·인스턴스 오버헤드에 있다. SVG 렌더러로 바꿔도 손댈 수 있는 몫이 프레임의 3분의 1 남짓이라는 뜻이라, 앞선 SVG 기각을 뒷받침한다.
(앞서 *"거꾸로다 … 1/5뿐"*이라고 적었다. 위 표 아래 주석 참고 — 근거 숫자는 틀렸지만 판단은 안 뒤집힌다: 3분의 1이어도 표면 교체로 손댈 몫이 절반을 못 넘는다.)
120px 폭에서도 1년치 일봉은 안 솎인다. 250점 × 50행 = 12,500점이 그대로 발행됐다 — 폭 120px의 임계는
width × pointsPerPixel= 480점이라 250은 그 아래다. "좁은 폭에서 데시메이션이 처음 물린다"는 경계가 여기서 판정났다: 안 물린다.
이 측정이 안 잡는 것 둘.
- rAF 팬아웃. 하네스는
inertScheduler를 쓰고 step이 행을 동기로 렌더한다. 재는 것은 렌더 작업이지 rAF 콜백 200개의 등록·디스패치 비용이 아니다. 0.90ms의 작업량에 비해 작을 것으로 보지만 재지는 않았다. - JS 힙.
performance.memory로는 못 잡는다 — 50행과 200행을 각각 세우고 rAF 3회 + 60ms를 쉰 뒤 읽어도 바이트까지 같은 값이 나온다 (Chrome이 캐시한다). CDP(Runtime.getHeapUsage)가 필요하고 그건 playwright를 직접 깔아야 한다. 비트맵 쪽은 산수로 확정된다 (docs/sparkline.md의 표: 50행 2.93MB vs 메인 차트 한 장 10.99MB).
판정. 인스턴스 수 축에서 재개봉 신호는 나타나지 않았다. 리스트 옆 스파크라인은 캔버스로 간다.