Skip to content

Commit 2a003ee

Browse files
committed
perf: 문서 그래프 조회 벤치마크 추가
1 parent 4868599 commit 2a003ee

3 files changed

Lines changed: 268 additions & 15 deletions

File tree

perf/read/README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
- 목록 테스트에 사용할 문서/브랜치/커밋 데이터를 생성
1313
- `perf/read/doc_list_benchmark.js`
1414
- `sidebar`, `full list`, `search` 읽기 성능 측정
15+
- `perf/read/doc_graph_benchmark.js`
16+
- 현재 MySQL projection 기반 graph 조회 성능 측정
1517

1618
## Why this benchmark
1719

@@ -43,7 +45,7 @@ RUN_ID=read01 \
4345
BASE_URL=http://localhost:8080 \
4446
USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \
4547
USER_COUNT=50 DOCS_PER_USER=5 \
46-
MAIN_COMMITS=8 FEATURE_COMMITS=5 BLOCKS_PER_COMMIT=100 \
48+
MAIN_COMMITS=8 FEATURE_BRANCHES=1 FEATURE_COMMITS=5 BLOCKS_PER_COMMIT=100 \
4749
SEED_VUS=20 \
4850
k6 run perf/seed/seed_dataset.js
4951
```
@@ -65,6 +67,7 @@ PERF_SEED_BLOCKS_PER_COMMIT=100
6567

6668
- `DOCS_PER_USER`: 5 이상
6769
- `MAIN_COMMITS`: 8 이상
70+
- `FEATURE_BRANCHES`: 1 이상. graph branch 수 확장 테스트에서는 이 값을 늘림
6871
- `FEATURE_COMMITS`: 5 이상
6972
- `BLOCKS_PER_COMMIT`: 100 이상
7073

@@ -110,3 +113,45 @@ SEARCH_PREFIX=PERF
110113
- `sidebar`보다 `full list`가 크게 느리면 preview 조립 비용 영향이 큼
111114
- `search`까지 더 느리면 목록 조립 비용 + 검색 쿼리 비용이 함께 작동
112115
- `PAGE_SIZE` 증가에 따라 p95가 급격히 오르면 per-doc 조립 비용이 병목일 가능성이 큼
116+
117+
## 5) Run graph benchmark
118+
119+
그래프 CQRS 적용 여부를 판단할 때는 먼저 현재 구조의 baseline을 측정합니다.
120+
Benchmark setup 단계에서 유저별 `/api/document` 목록을 조회해 테스트 대상 `docId`를 준비하고,
121+
실제 부하 구간에서는 `/api/document/{docId}/graph`만 반복 호출합니다.
122+
123+
```bash
124+
RUN_ID=graph01 \
125+
BASE_URL=http://localhost:8080 \
126+
USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \
127+
USER_COUNT=50 DOCS_PER_USER=5 \
128+
TITLE_PREFIX=PDEL \
129+
GRAPH_VUS=10 GRAPH_DURATION=30s \
130+
k6 run perf/read/doc_graph_benchmark.js
131+
```
132+
133+
Branch 수 확장용 seed 예시:
134+
135+
```bash
136+
RUN_ID=graph-branch-r1 \
137+
BASE_URL=http://localhost:8080 \
138+
USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \
139+
USER_COUNT=10 DOCS_PER_USER=2 \
140+
MAIN_COMMITS=30 FEATURE_BRANCHES=10 FEATURE_COMMITS=10 BLOCKS_PER_COMMIT=1 \
141+
SEED_VUS=2 \
142+
k6 run perf/seed/seed_dataset.js
143+
```
144+
145+
핵심 지표:
146+
147+
- `op_doc_graph_ms`
148+
- `op_doc_graph_payload_bytes`
149+
- `op_doc_graph_branch_count`
150+
- `op_doc_graph_commit_count`
151+
- `op_doc_graph_edge_count`
152+
153+
해석 기준:
154+
155+
- commit/edge 수 증가에 따라 `op_doc_graph_ms` p95/p99가 뚜렷하게 증가하면 graph read model 후보로 본다.
156+
- 응답 시간이 안정적인데 payload만 커진다면 CQRS보다 응답 축약, pagination, lazy loading을 먼저 검토한다.
157+
- commit 생성 직후 graph 즉시성이 중요하면 read model 조회 전환 시 MySQL fallback 또는 command 응답 보강이 필요하다.

perf/read/doc_graph_benchmark.js

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import http from 'k6/http';
2+
import exec from 'k6/execution';
3+
import { check } from 'k6';
4+
import { Rate, Trend } from 'k6/metrics';
5+
6+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
7+
const USER_PREFIX = __ENV.USER_PREFIX || 'perfuser';
8+
const USER_DOMAIN = __ENV.USER_DOMAIN || 'test.com';
9+
const USER_PASSWORD = __ENV.USER_PASSWORD || 'Testtest1';
10+
const USER_COUNT = Number(__ENV.USER_COUNT || 50);
11+
const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 5);
12+
const RUN_ID = __ENV.RUN_ID;
13+
const RESULT_DIR = __ENV.RESULT_DIR || 'perf/read/results';
14+
const TITLE_PREFIX = __ENV.TITLE_PREFIX || 'PDEL';
15+
const DOC_LIST_PAGE_SIZE = Number(__ENV.DOC_LIST_PAGE_SIZE || Math.max(DOCS_PER_USER * 2, 20));
16+
const DOC_LIST_MAX_PAGES = Number(__ENV.DOC_LIST_MAX_PAGES || 20);
17+
18+
if (!RUN_ID) {
19+
throw new Error('RUN_ID is required. Use the same RUN_ID that was used for the graph dataset seed');
20+
}
21+
22+
export const options = {
23+
summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'],
24+
scenarios: {
25+
graph_read: {
26+
executor: 'constant-vus',
27+
vus: Number(__ENV.GRAPH_VUS || 10),
28+
duration: __ENV.GRAPH_DURATION || '30s',
29+
exec: 'runGraphRead',
30+
},
31+
},
32+
thresholds: {
33+
http_req_failed: ['rate<0.02'],
34+
graph_failed: ['rate<0.02'],
35+
},
36+
};
37+
38+
const graphFailed = new Rate('graph_failed');
39+
const tLogin = new Trend('graph_login_ms');
40+
const tGraph = new Trend('op_doc_graph_ms');
41+
const tGraphPayload = new Trend('op_doc_graph_payload_bytes');
42+
const tGraphCommitCount = new Trend('op_doc_graph_commit_count');
43+
const tGraphEdgeCount = new Trend('op_doc_graph_edge_count');
44+
const tGraphBranchCount = new Trend('op_doc_graph_branch_count');
45+
46+
const cookieCache = new Map();
47+
48+
function pad3(n) {
49+
return String(n).padStart(3, '0');
50+
}
51+
52+
function userEmail(userNo) {
53+
return `${USER_PREFIX}_u${pad3(userNo)}@${USER_DOMAIN}`;
54+
}
55+
56+
function expectedTitlePrefix(userNo) {
57+
if (TITLE_PREFIX === 'PDEL') {
58+
return `PDEL-${RUN_ID}u${pad3(userNo)}d`;
59+
}
60+
return `PERF-${RUN_ID}-u${pad3(userNo)}-d`;
61+
}
62+
63+
function headers(cookie) {
64+
return {
65+
headers: {
66+
Cookie: cookie,
67+
},
68+
};
69+
}
70+
71+
function ensureStatus(res, statuses, op) {
72+
const ok = check(res, {
73+
[`${op} status`]: (r) => statuses.includes(r.status),
74+
});
75+
graphFailed.add(!ok, { op });
76+
if (!ok) {
77+
const snippet = typeof res.body === 'string' ? res.body.slice(0, 240) : '';
78+
throw new Error(`${op} failed status=${res.status}, body=${snippet}`);
79+
}
80+
}
81+
82+
function parseJson(res, op) {
83+
try {
84+
return res.json();
85+
} catch (_e) {
86+
throw new Error(`${op} invalid JSON`);
87+
}
88+
}
89+
90+
function login(email) {
91+
const cached = cookieCache.get(email);
92+
if (cached) {
93+
return cached;
94+
}
95+
96+
http.cookieJar().clear(BASE_URL);
97+
const res = http.post(
98+
`${BASE_URL}/api/user/login`,
99+
JSON.stringify({ email, password: USER_PASSWORD }),
100+
{ headers: { 'Content-Type': 'application/json' }, tags: { op: 'graph_login' } },
101+
);
102+
tLogin.add(res.timings.duration);
103+
ensureStatus(res, [200], 'graph_login');
104+
105+
const jsession = res.cookies.JSESSIONID && res.cookies.JSESSIONID[0];
106+
if (!jsession || !jsession.value) {
107+
throw new Error('graph_login missing JSESSIONID');
108+
}
109+
110+
const cookie = `JSESSIONID=${jsession.value}`;
111+
cookieCache.set(email, cookie);
112+
return cookie;
113+
}
114+
115+
function requestGraph(cookie, docId) {
116+
const res = http.get(
117+
`${BASE_URL}/api/document/${docId}/graph`,
118+
{ ...headers(cookie), tags: { op: 'doc_graph' } },
119+
);
120+
tGraph.add(res.timings.duration);
121+
tGraphPayload.add(typeof res.body === 'string' ? res.body.length : 0);
122+
ensureStatus(res, [200], 'doc_graph');
123+
124+
const body = parseJson(res, 'doc_graph');
125+
const valid = check(body, {
126+
'doc_graph has branches': (r) => Array.isArray(r.branches) && r.branches.length > 0,
127+
'doc_graph has commits': (r) => Array.isArray(r.commits),
128+
'doc_graph has edges': (r) => Array.isArray(r.edges),
129+
});
130+
graphFailed.add(!valid, { op: 'doc_graph_shape' });
131+
if (!valid) {
132+
throw new Error('doc_graph invalid graph response');
133+
}
134+
135+
tGraphBranchCount.add(body.branches.length);
136+
tGraphCommitCount.add(body.commits.length);
137+
tGraphEdgeCount.add(body.edges.length);
138+
}
139+
140+
function resolveDocsForUser(cookie, userNo) {
141+
const docs = [];
142+
const titlePrefix = expectedTitlePrefix(userNo);
143+
144+
for (let page = 0; page < DOC_LIST_MAX_PAGES && docs.length < DOCS_PER_USER; page += 1) {
145+
const res = http.get(
146+
`${BASE_URL}/api/document?page=${page}&size=${DOC_LIST_PAGE_SIZE}&sort=updatedAt&order=desc`,
147+
{ ...headers(cookie), tags: { op: 'graph_resolve_docs' } },
148+
);
149+
ensureStatus(res, [200], 'graph_resolve_docs');
150+
151+
const body = parseJson(res, 'graph_resolve_docs');
152+
if (!body || !Array.isArray(body.content)) {
153+
throw new Error('graph_resolve_docs invalid page response');
154+
}
155+
156+
for (const item of body.content) {
157+
if (item.title && item.title.startsWith(titlePrefix)) {
158+
docs.push({
159+
userNo,
160+
docId: item.id,
161+
title: item.title,
162+
});
163+
}
164+
}
165+
166+
if (body.last === true || body.content.length === 0) {
167+
break;
168+
}
169+
}
170+
171+
if (docs.length !== DOCS_PER_USER) {
172+
throw new Error(`graph_resolve_docs expected ${DOCS_PER_USER} docs for userNo=${userNo}, got ${docs.length}`);
173+
}
174+
175+
return docs;
176+
}
177+
178+
export function setup() {
179+
const docs = [];
180+
const cookies = {};
181+
for (let userNo = 1; userNo <= USER_COUNT; userNo += 1) {
182+
const cookie = login(userEmail(userNo));
183+
cookies[userNo] = cookie;
184+
docs.push(...resolveDocsForUser(cookie, userNo));
185+
}
186+
187+
return { docs, cookies };
188+
}
189+
190+
export function runGraphRead(data) {
191+
const index = exec.scenario.iterationInTest % data.docs.length;
192+
const doc = data.docs[index];
193+
const cookie = data.cookies[doc.userNo];
194+
if (!cookie) {
195+
throw new Error(`graph missing cookie for userNo=${doc.userNo}`);
196+
}
197+
requestGraph(cookie, doc.docId);
198+
}
199+
200+
export function handleSummary(data) {
201+
return {
202+
stdout: `\n[doc-graph-benchmark] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}\n`,
203+
[`${RESULT_DIR}/doc_graph_benchmark_${RUN_ID}.json`]: JSON.stringify(data, null, 2),
204+
};
205+
}

perf/seed/seed_dataset.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const USER_PASSWORD = __ENV.USER_PASSWORD || 'Testtest1';
1010
const USER_COUNT = Number(__ENV.USER_COUNT || 50);
1111
const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 3);
1212
const MAIN_COMMITS = Number(__ENV.MAIN_COMMITS || 6);
13+
const FEATURE_BRANCHES = Number(__ENV.FEATURE_BRANCHES || 1);
1314
const FEATURE_COMMITS = Number(__ENV.FEATURE_COMMITS || 4);
1415
const BLOCKS_PER_COMMIT = Number(__ENV.BLOCKS_PER_COMMIT || 20);
1516
const RUN_ID = __ENV.RUN_ID || Math.floor(Date.now() / 1000).toString(36);
@@ -196,33 +197,35 @@ export default function () {
196197
mainCommits.push(commit.id);
197198
}
198199

199-
if (mainCommits.length < 2) {
200+
if (FEATURE_BRANCHES <= 0 || mainCommits.length < 2) {
200201
return;
201202
}
202203

203-
const featureName = `feat-${key}`;
204-
const fromCommitId = mainCommits[0]; // non-leaf from commit
204+
for (let branchNo = 1; branchNo <= FEATURE_BRANCHES; branchNo += 1) {
205+
const featureName = `feat-${branchNo}-${key}`.slice(0, 100);
206+
const fromCommitId = mainCommits[Math.min(branchNo - 1, mainCommits.length - 2)];
205207

206-
const branchRes = createBranch(cookie, doc.id, featureName, fromCommitId);
207-
let featureBranchId = branchRes.branchId;
208+
const branchRes = createBranch(cookie, doc.id, featureName, fromCommitId);
209+
let featureBranchId = branchRes.branchId;
208210

209-
if (!featureBranchId) {
210-
const graph1 = getGraph(cookie, doc.id);
211-
featureBranchId = findBranchIdByName(graph1, featureName);
212211
if (!featureBranchId) {
213-
throw new Error('seed_branch_create could not resolve feature branch id');
212+
const graph1 = getGraph(cookie, doc.id);
213+
featureBranchId = findBranchIdByName(graph1, featureName);
214+
if (!featureBranchId) {
215+
throw new Error('seed_branch_create could not resolve feature branch id');
216+
}
214217
}
215-
}
216218

217-
for (let i = 0; i < FEATURE_COMMITS; i += 1) {
218-
const title = `f${i}-${key}`.slice(0, 28);
219-
createCommit(cookie, doc.id, featureBranchId, title);
219+
for (let i = 0; i < FEATURE_COMMITS; i += 1) {
220+
const title = `f${branchNo}-${i}-${key}`.slice(0, 28);
221+
createCommit(cookie, doc.id, featureBranchId, title);
222+
}
220223
}
221224
}
222225

223226
export function handleSummary(data) {
224227
const summary = {
225-
stdout: `\n[seed-dataset] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, total_docs=${TOTAL_DOCS}\n`,
228+
stdout: `\n[seed-dataset] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, feature_branches=${FEATURE_BRANCHES}, total_docs=${TOTAL_DOCS}\n`,
226229
};
227230

228231
if (RESULT_DIR) {

0 commit comments

Comments
 (0)