Skip to content

성능·보존 기준선 — 기준 SHA 에서 잰 숫자 (이슈 #1284, G04)

한 문장 — v0.18.0 구조 개선 의 숫자다. 같은 fixture·같은 PC·같은 절차로 R04 가 다시 재서 전후를 비교한다. 여기 숫자는 목표가 아니라 출발점이고, 릴리스 상한은 릴리스 예산에 따로 있다. 미측정은 미측정으로 적었다(0 이 아니다).

  • 이슈: #1284 (계획 ID G04). 규격: 진단 규격. 도구: scripts/performance/{workload,measure,probe}/*.mjs(재실행 절차 §5).
  • 기준 SHA: 앱·서버 16b8e996(main, 2026-09-07 — D03 #1312 뒤; 이 트랙의 코드는 측정 대상이 아니다). Production 프로브 시점의 배포도 같은 상태(v0.17.1 릴리스 전, main = staging).
  • 증거 파일: scripts/performance/evidence/{workloads/<프로필>/(workload|load-result|measure-db|measure-browser).json, production/<sha>.json} — 저장소 밖(.gitignore). 표는 node scripts/performance/measure/summarize.mjs 가 낸 것을 그대로 붙였다.

1. 읽는 법 — 유저 A 의 하루로

A 가 폰에서 운동을 저장한다. 이 문서는 그 한 건이 지나는 자리마다 "지금 얼마나 걸리는가" 를 적는다: ① 저장 문이 서버에서 끝나기까지(쓰기 p95) ② 통계 잡이 큐에서 기다렸다가 처리되기까지(큐 대기·소진) ③ 홈 숫자가 새 세대를 보기까지(요청→반영) ④ 다음날 앱을 열 때 화면이 뜨기까지(브라우저 초기 비용) ⑤ 홈·달력·리포트 RPC 가 서버에서 답하기까지(읽기 p95). 그리고 그 사이 서버가 얼마나 일했는가(WAL·행 갱신). Production 은 실제 5명의 기록으로, 샌드박스는 1·4·10년 fixture 로 잰다.

2. 조건 (이 숫자가 유효한 범위)

조건샌드박스Production
Postgres17.6.1.127 (Supabase 로컬 이미지, package.json supabaseToolchain)17.6 (aarch64)
인스턴스Docker Desktop VM 24 vCPU · 31GB(호스트 AMD Ryzen 9 5900X 12코어 24스레드 · 69GB · Windows 11) — 측정 중 다른 세션의 Supabase 컨테이너 130여 개가 함께 떠 있었다max_connections 60 · shared_buffers 256MB · effective_cache_size 768MB(약 1GB 인스턴스 — compute 등급은 대시보드 확인 대기) · 도쿄 리전
DB 설정기본(track_functions none)track_functions none · track_io_timing off · pg_stat_statements.track top · statement_timeout 2min · wal_level logical
workercron 없음 → 러너가 process_user_exercise_stats_refresh_jobs(25) 를 직접 호출(배치 25 = Production cron 과 같은 값)lift-guild-stats-refresh 매분 · pr-overview-snapshot-rollover 매분
시간 측정서버 시계(clock_timestamp) — PostgREST·네트워크·TLS 비용 없음서버 통계 표·cron 이력·잡 행의 시각 차
브라우저Chromium(Playwright) · CPU 4배 느리게 · 미리보기 빌드(vite preview) · 로컬 스택 — Vercel 엣지·실기기 아님실기기 숫자 없음(perf_tab_switch 이벤트만)
캐시 상태읽기 20회 반복(첫 회 포함 분위수) — DB 캐시 적중률 99.9% 이상 = warm실사용

3. Production 실측 (읽기 전용 프로브)

항목값 (2026-09-07T01:17:42.558Z, release e41fe1ea)
Postgres / 연결 / shared_buffers / effective_cache17.6 / 60 / 256MB / 768MB
track_functions / track_io_timing / pg_stat_statements.track / statement_timeoutnone / off / top / 2min
DB 크기405 MB
사용자 / 기록 있는 사용자5 / 5
완료 세션 (앱 / 전체) · 계획64 / 2018 · 7
세부 종목 / 세트 / 세부 세트 / 영수증4199 / 15878 / 16704 / 1863
기록 기간2021-11-11 ~ 2026-09-06
사용자당 세션 p50 / p95 / 최대 · 최대 기간(일)331 / 1023 / 1023 · 1760
7일 실제 세션 / 영수증 / 쓰기 사용자 · 30일 영수증12 / 340 / 4 · 1350
잡 상태 · 열린 잡 · 최대 시도{"failed":3,"completed":1624} · 0 · 3
잡 큐 대기 p50 / p95 / max (ms, 30일)59931 / 60022 / 202886
잡 처리 시간 p95 (ms) · 0 으로 기록된 수 / 전체0 · 1362 / 1362
잡 실패 30일
세대: stale owner / 최대 lag / 요청→반영 p50 / p95 / max (ms)0 / 0 / 9226 / 59909 / 59909
WAL 누계(통계 리셋 2026-08-18 14:39:17.139507+00 이후)53 GB · records 390363522
클라이언트 오류 7일 / 30일 · 30일 release 종류96 / 297 · 58
cron barbelic-client-error-purge 24h (succeeded)1회 · p50 61 / p99 61 / max 61 ms · 5초 초과 0
cron barbelic-exercise-usage-stats-refresh 24h (succeeded)24회 · p50 50 / p99 114 / max 114 ms · 5초 초과 0
cron barbelic-stats-backfill-scan 24h (succeeded)24회 · p50 321 / p99 803 / max 803 ms · 5초 초과 0
cron barbelic-wodup-failed-staging-purge 24h (succeeded)1회 · p50 72 / p99 72 / max 72 ms · 5초 초과 0
cron lift-guild-pr-overview-snapshot-rollover 24h (succeeded)1440회 · p50 25 / p99 67 / max 36341 ms · 5초 초과 3
cron lift-guild-stats-refresh 24h (succeeded)1440회 · p50 18 / p99 23606 / max 31895 ms · 5초 초과 24
churn user_exercise_period_statslive 10728 · ins 6721440 · upd 14672724 · del 6692894 · autovacuum 445
churn user_exercise_strength_observationslive 15374 · ins 5520937 · upd 13938924 · del 5484439 · autovacuum 445
churn user_exercise_session_rollupslive 3968 · ins 1357881 · upd 7517105 · del 1347061 · autovacuum 456
churn user_exercise_recordslive 3968 · ins 1357881 · upd 1920302 · del 1347061 · autovacuum 436
churn user_exercise_max_rep_observationslive 3602 · ins 1362629 · upd 1723776 · del 1358859 · autovacuum 427
WAL 상위 문장select public.run_user_exercise_stats_refresh_cron($1) · 27988회 · 평균 302.8ms · WAL 46074MB
WAL 상위 문장WITH pgrst_source AS (SELECT pgrst_call.pgrst_scalar FROM (SELECT $1 AS json_dat · 165회 · 평균 2508.9ms · WAL 1458MB
WAL 상위 문장WITH pgrst_source AS (SELECT pgrst_call.pgrst_scalar FROM (SELECT $1 AS json_dat · 116회 · 평균 1066.5ms · WAL 357MB

무슨 뜻인가

  • 통계 잡의 큐 대기 p50 59.9초·p95 60.0초 — 잡은 거의 즉시 생기지만 다음 분 cron 이 집을 때까지 기다린다. freshness 바닥이 cron 주기(60초)다. 요청→반영 p50 9.2초는 잡 처리 자체가 아니라 요청 시각과 cron 시각의 차다.
  • 잡 처리 시간이 전부 0 — 뷰가 processed_at − processing_started_at 을 빼는데 두 값이 같은 트랜잭션의 now() 라 항상 0 이다(측정 결함, D08). Production 도 샌드박스도 같다.
  • 매분 cron run_user_exercise_stats_refresh_cron 이 20일 동안 WAL 46GB(전체의 85%)를 썼고 투영 테이블은 살아 있는 행 1만 개에 갱신 1,467만·삭제 669만 회 — 재계산이 투영을 통째로 지우고 다시 쓴다(D08/D11 입력). cron 은 대부분 20ms 안에 끝나지만 하루 24회는 5초를 넘고 최대 32초다.
  • 5명·2,018세션(앱 작성 64) 규모라 요청률은 사실상 smoke·e2e 가 만든다(30일 영수증 1,350건 중 실제 세션 12건/7일). 부하 조건은 §4 의 fixture 가 대신한다.

4. 샌드박스 workload 실측

프로필사용자기록세션세트쓰기 p50/p95/max (ms)적재 결과
history-10y1×기기 110년26093130819.6 / 27.8 / 107.3ok · 거부 0 · 재생 0 · 충돌 0/0
history-1y1×기기 11년260312020.1 / 26.4 / 47ok · 거부 0 · 재생 0 · 충돌 0/0
history-4y1×기기 14년10431251620.5 / 28.7 / 86ok · 거부 0 · 재생 0 · 충돌 0/0
mixed-read-write-import5×기기 14년57156858018.8 / 30.9 / 26465.1ok · 거부 0 · 재생 0 · 충돌 0/0
multi-owner-1010×기기 11년26003120018.8 / 26 / 86.7ok · 거부 0 · 재생 0 · 충돌 0/0
same-owner-two-devices1×기기 21년260312019.4 / 27.4 / 63.3ok · 거부 0 · 재생 60 · 충돌 26/26
읽기 RPC p95 (ms)history-10yhistory-1yhistory-4ymixed-read-write-importmulti-owner-10same-owner-two-devices
get_home_dashboard18.5 (29KB)10.9 (29KB)90 (29KB)12.1 (29KB)12 (29KB)41.5 (29KB)
get_calendar_month_summary17.5 (12KB)7.3 (12KB)35.4 (12KB)7.8 (12KB)7.3 (12KB)8.1 (12KB)
get_calendar_day_summary8 (1KB)7 (1KB)10.5 (1KB)7.9 (1KB)7.9 (1KB)9.6 (1KB)
get_session_detail15.1 (21KB)12 (23KB)13 (19KB)10.9 (19KB)20.5 (23KB)22.5 (23KB)
get_pr_overview9.5 (63KB)9.1 (63KB)9.4 (63KB)7.3 (63KB)7.8 (63KB)7.2 (63KB)
get_volume_overview3104 (1557KB)164.2 (618KB)1218.8 (1180KB)366.5 (1182KB)170.7 (618KB)267.5 (618KB)
get_profile_feed43.9 (82KB)53.9 (82KB)35.1 (82KB)36.8 (82KB)53.2 (82KB)46.5 (82KB)
get_session_search229.9 (480KB)276.9 (479KB)804.4 (480KB)255.9 (480KB)210.5 (479KB)217.6 (480KB)
get_user_manual_records_v10.5 (0KB)0.6 (0KB)0.6 (0KB)0.5 (0KB)0.6 (0KB)0.7 (0KB)
잡·세대·DBhistory-10yhistory-1yhistory-4ymixed-read-write-importmulti-owner-10same-owner-two-devices
잡 수(적재 뒤)1117101
큐 대기 p95 (ms)5837865132474967069449909634
잡 처리 시간 p95 (ms, 뷰)000000
잡 처리 시간 0 으로 기록된 수1117101
backlog 소진 wall (ms)12762544952852175169435244823
worker 라운드 최대 (ms)127617449128516미측정435054820
요청→반영 p50 (ms)8477628032274912313843
stale owner000000
소진 WAL (MB)394.735.5140.9705306.535.3
소진 행 갱신(upd)3635933658514556062650529319636585
읽기 20회 blks_hit533862013579321951062123126018139041661930
캐시 적중률 (%)99.910010099.899.899.9
lock wait 관측(대기 backend 최대)000000
브라우저 초기 비용coldwarm
history-1y 홈 그려짐 (ms)965995
history-1y load 이벤트 (ms)304230.1
history-1y domInteractive (ms)54.733.4
history-1y 전송 바이트(문서)2568300
history-1y script 수 / KB40 / 53440 / 12
history-1y 가장 느린 script (ms)24.210.2
history-1y DOM 노드267267
history-1y RPC 호출 수98
history-1y RPC p50 (ms, 브라우저 왕복)get_profile_feed 43.8, get_pr_overview 51, get_user_exercise_favorites 45.5, get_home_dashboard 204.1, get_exercise_search_signals_v1 20.1, get_calendar_month_summary 17.7, get_calendar_day_summary 25.6, get_volume_overview 25.7, get_exercise_catalog 631.3get_pr_overview 455, get_profile_feed 460.3, get_home_dashboard 464.7, get_user_exercise_favorites 461.7, get_exercise_search_signals_v1 17.7, get_calendar_month_summary 18.4, get_calendar_day_summary 20.1, get_volume_overview 39.4

무슨 뜻인가

  • 쓰기(저장 문 서버 시간)는 기록량과 무관하게 p95 22~28ms — 저장 자체는 싸다. 비용은 그 뒤 통계 재계산에 있다.
  • 읽기 가운데 get_volume_overview 가 기록량에 비례해 자란다: 1년 170ms → 4년 1,081ms → 10년 3,002ms(응답 1.5MB). CI 시간 예산(1,000ms)은 4년부터 넘는다 — CI 는 in-process mock 으로 재므로 이 사실을 모른다. get_session_search 도 4년에서 661ms, 혼합 부하에서 1,041ms. 나머지 화면 RPC 는 20ms 안팎.
  • 통계 backlog 소진은 세션 수에 비례한다: 1년 4.8초(1잡) → 4년 27초 → 10년 128초(worker 한 라운드 127초 — Production statement_timeout 2분과 같은 크기). WAL 은 10년 소진에 353MB, 행 갱신 36만 회. 잡 하나가 사용자 전체 이력을 다시 계산한다.
  • 같은 owner 두 기기: 재생 60건 전부 replayed=true, 낡은 개정번호 수정 26건 전부 40001/LG409 — 멱등·충돌 규칙은 이 부하에서 그대로 성립.
  • owner 10명이 같은 분에 쓰면 잡 10개가 각각 생기고 owner 별로 처리된다. lock wait 는 이 방법(100ms 샘플링)으로는 관측되지 않았다 — 정확한 잠금 대기 시간은 log_lock_waits 가 없어 미측정.
  • 인입 근사(저장 문 500회 연속)는 정식 인입 경로(staging → canonical)가 아니다. 인입의 파일 쪽 비용(브라우저 사전검사·SHA-256·worker 정규화)은 I01(#1415, 2026-09-10)이 npm run perf:import 로 쟀다 — 10,000세션·24.3MB 파일: 사전검사 0.29~0.33초(이벤트 루프 최대 정지 47ms·힙 최고 55MB), 정규화 0.92~0.94초(힙 최고 226MB = Edge 256MB 의 88%); 2,000세션·4.9MB: 사전검사 66ms·정규화 0.22초·힙 52MB. 값은 자원 예산 §3-5. staging → canonical 의 DB 비용은 여전히 미측정.

5. 다시 재는 법 (R04 가 같은 절차로)

bash
# 1) 샌드박스(같은 Postgres 이미지) — 다른 세션의 스택을 쓰지 않는다
npm run ci:local -- --full --only db --sandbox <>
export BARBELIC_DB_SANDBOX=<폴더>
# 2) workload 생성(결정적) → 적재 → DB 측정, 프로필마다
node scripts/performance/workload/generate.mjs --all
for p in history-1y history-4y history-10y same-owner-two-devices multi-owner-10 mixed-read-write-import; do
  node scripts/performance/workload/load.mjs --workload scripts/performance/evidence/workloads/$p
  node scripts/performance/measure/db.mjs --workload scripts/performance/evidence/workloads/$p --iterations 20
done
# 3) 브라우저 초기 비용 — 미리보기 빌드 + e2e 환경변수(supabase status -o env)
npm run build && node node_modules/vite/bin/vite.js preview --port 4173 &
E2E_SUPABASE_URL= E2E_SUPABASE_ANON_KEY= E2E_SUPABASE_SERVICE_ROLE_KEY= E2E_APP_URL=http://127.0.0.1:4173 \
  node --import tsx scripts/performance/measure/browser.mjs --workload scripts/performance/evidence/workloads/history-1y --seed-sessions 120
# 4) Production 읽기 전용 프로브(supabase CLI 로그인 토큰)
node scripts/performance/probe/production.mjs
# 5) 표
node scripts/performance/measure/summarize.mjs

비교 규칙: 같은 프로필·같은 PC·같은 반복 수로 두 번 이상 재고 분위수(p50·p95·max)와 표본 수 를 같이 적는다. 다른 PC 의 절대값을 비교하지 않는다 — 같은 PC 에서 전/후 비율(릴리스 예산 §2)로 판정한다.

6. 미측정과 결함 (숫자로 쓰지 않은 것)

항목상태담당
잡 처리 시간(duration_ms)결함으로 전부 0 — processing_started_at·processed_at 이 같은 트랜잭션 now(). clock_timestamp() 로 바꿔야 잰다D08
RPC 별 서버 시간(Production)pg_stat_statements 가 PostgREST 래핑(pgrst_source … $1)만 보여 이름을 못 붙인다. track_functions = pl 뒤에 pg_stat_user_functionsD08/HQ(DB 설정)
lock wait(ms)pg_locks 샘플링으로 대기 backend 수만. 시간은 log_lock_waits 필요D08
인입(정식 경로)파일 쪽(사전검사·해시·정규화)은 I01 이 Node 로 실측(npm run perf:import, 자원 예산 §3-5). staging → canonical 의 DB 비용과 브라우저 실기기 정지는 미측정I01 → R04
브라우저 실기기·Vercel 엣지미리보기 빌드·로컬 스택 값만U05/B02
복구 RPO/RTO사본은 매일 1회(암묵 RPO 24시간)·복원 리허설 0회 → RTO 미측정. 평가 방법은 릴리스 예산 §5S11/R03
Production compute 등급shared_buffers 256MB 로 약 1GB 추정 — 대시보드 확인 대기(오너 손)오너