Skip to content

성능 기준

이 문서가 이 라이브러리의 성능 규범이다 — 무엇을 재고, 무엇에 견주고, 언제 행동하는가. 결정의 역사는 여기 없다: 왜 지금 최적화하지 않는가, 구현했다 되돌린 후보 셋의 값과 대가는 ADR-0011이 갖는다. 이 문서는 기준과 대장을 갖고, 측정이 쌓이면 대장이 자란다.

수립 경위: senior-review-2026-08-11.

예산 — 이중선

어기면
60Hz 바닥프레임 16.7ms결함이다. 원인을 격리(짝 비교)하고 고친다
120Hz 감시선프레임 8.3msADR-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.jsonpackages/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프레임
연속 x3.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문 끔문 켬
연속 x22.80 ms14.70 / 13.20 ms
봉 번호28.70 ms19.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

비어 있던 칸을 처음 쟀다. 실시간 갱신 × 파생 시리즈가 만나는 자리, 그리고 그 자리의 봉 번호 좌표계 짝이다. 둘 다 예산 밖이다.

시나리오명령 점중앙값p95commit%여유(60Hz)
틱 추가 · 100k, 지표 4개 (연속 x)33,02218.70 ms25.5012%0.9×
틱 추가 · 100k, 지표 4개 (봉 번호)33,022115.20 ms122.402%0.14×
팬 · 100k, 지표 4개 (봉 번호)32,71826.50 ms29.709%0.6×

같은 세션의 연속 x 짝: 팬 6.90 · 호버 3.80.

commit%가 진단을 다 말한다. 봉 번호 틱은 프레임의 98%가 그리기 밖이다 — 같은 명령 수(33,022)를 그리는 호버가 3.80ms인데 틱이 115ms다. 그리는 일은 그대로고 데이터 경로가 100ms를 먹는다. 원인은 Plot.rebuildX가 틱마다 전 시리즈의 x를 통째로 다시 세우는 것이다(x-mapping.tssources.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. 솎기가 창의 점마다 화면 자리를 묻는데(screenPlaceOftoDomain) 그때마다 xs 전체를 뒤졌다 — 10만 점 창이면 프레임당 10만 × log 10만이다. 훑기는 오름차순이라 직전 자리에서 시작하게 했다(답은 같다):

시나리오힌트 끔힌트 켬대조군(연속 x)
팬 · 100k, 지표 4개 (봉 번호)26.20 ms11.70 ms6.80 ↔ 6.80
틱 갱신 · 100k, 지표 4개 (봉 번호)40.90 ms33.50 ms19.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, 한 세션, 빌드 후 실행.

시나리오명령 점중앙값p95commit%여유(60Hz)여유(120Hz)
호버 · 100k 캔들3,6400.60 ms0.8043%27.8×13.8×
팬 · 100k 캔들3,6361.00 ms1.2029%16.7×8.3×
줌 · 100k 캔들3,6341.00 ms1.3029%16.7×8.3×
틱 추가 · 100k 캔들3,6361.20 ms1.4024%13.9×6.9×
호버 · 100k, 지표 4개33,0223.40 ms4.1055%4.9×2.4×
팬 · 100k, 지표 4개32,7146.90 ms7.8028%2.4×1.2×
호버 · 100k, 지표4+드로잉+툴팁 (R3b)33,0473.40 ms4.1058%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 바닥은 전 시나리오 여유. 두 곳이 주시 구간이다:

  1. 팬 · 100k, 지표 4개 — 120Hz 여유 1.2×. ADR-0011 재개봉 신호가 가리키던 바로 그 구성이다. 짝 비교상 오토스케일 몫은 노이즈 안이라 원인은 파생 시리즈의 명령량(33k점) 자체다. 아직 감시선 안이고 실사용 드롭 보고도 없으므로 재개봉하지 않는다 — 다음 행에서 다시 본다.
  2. 기준 콜드 스타트 — 여유 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% 이내), 빌드 후 실행.

시나리오명령 점중앙값p95commit%여유(60Hz)여유(120Hz)
호버 · 100k 캔들3,6400.60 ms0.70~0.9041~47%27.8×13.8×
팬 · 100k 캔들3,6361.00 ms1.10~1.5028~30%16.7×8.3×
줌 · 100k 캔들3,6340.90~1.00 ms1.10~1.2027~31%~17.6×~8.7×
틱 추가 · 100k 캔들3,6361.20 ms1.4023~25%13.9×6.9×
호버 · 100k, 지표 4개33,0263.30~3.40 ms3.60~4.5055~58%~5.0×~2.5×
팬 · 100k, 지표 4개32,7186.60~6.70 ms7.40~7.9027~29%~2.5×1.24~1.26×
호버 · 100k, 지표4+드로잉+툴팁 (R3b)33,0513.30~3.40 ms3.60~4.4057~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마다 하나씩 새로 발행하는 것과 개수가 맞는다 — 회귀가 아니라 의도된 신규 그리기.

주시 재판정.

  1. 팬 · 100k, 지표 4개 — 1.24~1.26×, 08-11(1.2×)과 같은 자리. 짝 비교상 원인도 그대로(파생 명령량 32.7k점 자체). 재개봉 신호로 넘어가지 않았다 — 경계선에 계속 붙어 있으므로 다음 행에서 다시 본다.
  2. 기준 콜드 스타트 — 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회 런.

시나리오명령 점중앙값p95commit%여유(60Hz)
틱 · 스파크라인 50행 × 30점1,5000.20~0.30 ms0.30~0.4036%~60×
틱 · 스파크라인 50행 × 250점12,5001.00 ms1.20~1.4020%16.7×
틱 · 스파크라인 200행 × 30점6,0000.90 ms1.00~1.1037%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).

읽어야 할 것 넷.

  1. 선형이고 상수가 작다. 인스턴스 하나의 한계 비용이 콜드 스타트 ~25 µs, 프레임 ~4 µs다. 200행 리스트가 서는 데 4.8ms인데, 같은 세션에서 지표 넷 얹은 차트 하나가 14.9ms다 — 리스트 전체가 큰 차트 하나의 1/3이다.

  2. 동시 틱도 예산의 5%다. 200행이 한 프레임에 전부 갱신되어도 0.90ms고, 같은 세션 호버·지표4(3.30ms)의 **27%**다.

  3. commit 비중이 여전히 낮다 — 큰 차트는 호버에서 55~58%인데 리스트는 20~37%다. 작은 캔버스 N개에서는 비용이 재생이 아니라 서술·인스턴스 오버헤드에 있다. SVG 렌더러로 바꿔도 손댈 수 있는 몫이 프레임의 3분의 1 남짓이라는 뜻이라, 앞선 SVG 기각을 뒷받침한다.

    (앞서 *"거꾸로다 … 1/5뿐"*이라고 적었다. 위 표 아래 주석 참고 — 근거 숫자는 틀렸지만 판단은 안 뒤집힌다: 3분의 1이어도 표면 교체로 손댈 몫이 절반을 못 넘는다.)

  4. 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).

판정. 인스턴스 수 축에서 재개봉 신호는 나타나지 않았다. 리스트 옆 스파크라인은 캔버스로 간다.