Skip to content

Commit 65194ff

Browse files
authored
Merge: preview와 썸네일 워크플로우 성능 테스트 추가
test: preview와 썸네일 워크플로우 성능 테스트 추가
2 parents 80858d1 + 1bcffdc commit 65194ff

9 files changed

Lines changed: 527 additions & 24 deletions

File tree

infra/docker-compose.stg.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ services:
136136
image: prom/prometheus:latest
137137
container_name: prometheus-stg
138138
restart: unless-stopped
139+
command:
140+
- "--config.file=/etc/prometheus/prometheus.yml"
141+
- "--storage.tsdb.path=/prometheus"
142+
- "--web.enable-remote-write-receiver"
143+
ports:
144+
- "${PROMETHEUS_BIND_ADDR:-127.0.0.1}:9090:9090"
139145
depends_on:
140146
- cadvisor
141147
volumes:

perf/delete/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
## Files
1010

11-
- `perf/delete/seed_dataset.js`
12-
- 유저별 문서/커밋/브랜치 데이터 생성
11+
- `perf/seed/seed_dataset.js`
12+
- 유저별 문서/커밋/브랜치 데이터 생성. delete/read/thumbnail E2E에서 공용으로 사용
1313
- `perf/delete/delete_only_benchmark.js`
1414
- 생성된 문서를 찾아 삭제만 수행
1515
- `perf/delete/compare_delete_summary.mjs`
@@ -69,7 +69,7 @@ USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \
6969
USER_COUNT=100 DOCS_PER_USER=3 \
7070
MAIN_COMMITS=6 FEATURE_COMMITS=4 BLOCKS_PER_COMMIT=20 \
7171
SEED_VUS=20 \
72-
k6 run perf/delete/seed_dataset.js
72+
k6 run perf/seed/seed_dataset.js
7373
```
7474

7575
생성 규칙:
@@ -103,7 +103,7 @@ k6 run perf/delete/delete_only_benchmark.js \
103103
1. 브랜치 checkout
104104
2. 서버 실행
105105
3. 유저 준비(`PERF_SEED_USER_COUNT`, `PERF_SEED_DOCS_PER_USER=0` 적용된 상태)
106-
4. 데이터 생성 (`seed_dataset.js`)
106+
4. 데이터 생성 (`perf/seed/seed_dataset.js`)
107107
5. 삭제 벤치 실행 (`delete_only_benchmark.js`)
108108
6. 결과 파일 저장
109109

perf/read/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## Files
1010

11-
- `perf/delete/seed_dataset.js`
11+
- `perf/seed/seed_dataset.js`
1212
- 목록 테스트에 사용할 문서/브랜치/커밋 데이터를 생성
1313
- `perf/read/doc_list_benchmark.js`
1414
- `sidebar`, `full list`, `search` 읽기 성능 측정
@@ -35,7 +35,7 @@ PERF_SEED_DOCS_PER_USER=0
3535

3636
## 2) Seed dataset
3737

38-
삭제 벤치에서 쓰던 seed 스크립트를 그대로 재사용합니다.
38+
공용 seed 스크립트를 사용합니다.
3939
중요한 건 목록 조회 시 문서별 branch/commit이 충분히 많아지도록 데이터를 만드는 것입니다.
4040

4141
```bash
@@ -45,7 +45,7 @@ USER_PREFIX=perfuser USER_DOMAIN=test.com USER_PASSWORD=Testtest1 \
4545
USER_COUNT=50 DOCS_PER_USER=5 \
4646
MAIN_COMMITS=8 FEATURE_COMMITS=5 BLOCKS_PER_COMMIT=100 \
4747
SEED_VUS=20 \
48-
k6 run perf/delete/seed_dataset.js
48+
k6 run perf/seed/seed_dataset.js
4949
```
5050

5151
애플리케이션 initializer로 DB에 직접 주입할 수도 있습니다. 이 방식은 API 호출 비용 없이
@@ -89,7 +89,7 @@ SEARCH_PREFIX=PERF
8989

9090
결과 파일:
9191

92-
- `perf/read/results/doc_list_benchmark_<RUN_ID>.json`
92+
- `perf/read/results/<test-name>/.../*.json`
9393

9494
핵심 지표:
9595

perf/read/doc_list_benchmark.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const USER_COUNT = Number(__ENV.USER_COUNT || 50);
1111
const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 3);
1212
const PAGE_SIZE = Number(__ENV.PAGE_SIZE || 10);
1313
const RUN_ID = __ENV.RUN_ID;
14+
const DATASET_RUN_ID = __ENV.DATASET_RUN_ID || RUN_ID;
1415
const RESULT_DIR = __ENV.RESULT_DIR || 'perf/read/results';
1516
const SEARCH_PREFIX = __ENV.SEARCH_PREFIX || 'PDEL';
1617

@@ -19,6 +20,7 @@ if (!RUN_ID) {
1920
}
2021

2122
export const options = {
23+
summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'],
2224
scenarios: {
2325
sidebar_list: {
2426
executor: 'constant-vus',
@@ -70,9 +72,9 @@ function userEmail(userNo) {
7072

7173
function keywordForUser(userNo) {
7274
if (SEARCH_PREFIX === 'PERF') {
73-
return `PERF-${RUN_ID}-u${pad3(userNo)}`;
75+
return `PERF-${DATASET_RUN_ID}-u${pad3(userNo)}`;
7476
}
75-
return `${SEARCH_PREFIX}-${RUN_ID}u${pad3(userNo)}`;
77+
return `${SEARCH_PREFIX}-${DATASET_RUN_ID}u${pad3(userNo)}`;
7678
}
7779

7880
function headers(cookie) {
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const MAIN_COMMITS = Number(__ENV.MAIN_COMMITS || 6);
1313
const FEATURE_COMMITS = Number(__ENV.FEATURE_COMMITS || 4);
1414
const BLOCKS_PER_COMMIT = Number(__ENV.BLOCKS_PER_COMMIT || 20);
1515
const RUN_ID = __ENV.RUN_ID || Math.floor(Date.now() / 1000).toString(36);
16+
const RESULT_DIR = __ENV.RESULT_DIR || '';
1617

1718
const TOTAL_DOCS = USER_COUNT * DOCS_PER_USER;
1819

@@ -220,8 +221,13 @@ export default function () {
220221
}
221222

222223
export function handleSummary(data) {
223-
return {
224+
const summary = {
224225
stdout: `\n[seed-dataset] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, total_docs=${TOTAL_DOCS}\n`,
225-
'perf/delete/results/seed_dataset_summary.json': JSON.stringify(data, null, 2),
226226
};
227+
228+
if (RESULT_DIR) {
229+
summary[`${RESULT_DIR}/seed_dataset_summary.json`] = JSON.stringify(data, null, 2);
230+
}
231+
232+
return summary;
227233
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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 || 'perfe2e';
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 || 1);
11+
const DOCS_PER_USER = Number(__ENV.DOCS_PER_USER || 20);
12+
const RUN_ID = __ENV.RUN_ID || 'e2e_u1d20';
13+
const E2E_VUS = Number(__ENV.E2E_VUS || 1);
14+
const E2E_DURATION = __ENV.E2E_DURATION || '30s';
15+
const RESULT_DIR = __ENV.RESULT_DIR || 'perf/thumbnail/results';
16+
17+
export const options = {
18+
summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'max'],
19+
scenarios: {
20+
preview_e2e: {
21+
executor: 'constant-vus',
22+
vus: E2E_VUS,
23+
duration: E2E_DURATION,
24+
exec: 'runPreviewE2E',
25+
},
26+
},
27+
thresholds: {
28+
http_req_failed: ['rate<0.02'],
29+
preview_e2e_failed: ['rate<0.02'],
30+
},
31+
};
32+
33+
const e2eFailed = new Rate('preview_e2e_failed');
34+
const tLogin = new Trend('preview_login_ms');
35+
const tSetupDocList = new Trend('preview_setup_doc_list_ms');
36+
const tSetupGraph = new Trend('preview_setup_graph_ms');
37+
const tSaveUpdate = new Trend('op_preview_save_update_ms');
38+
const tDocList = new Trend('op_preview_doc_list_ms');
39+
const tTotal = new Trend('op_preview_e2e_total_ms');
40+
41+
function pad3(n) {
42+
return String(n).padStart(3, '0');
43+
}
44+
45+
function userEmail(userNo) {
46+
return `${USER_PREFIX}_u${pad3(userNo)}@${USER_DOMAIN}`;
47+
}
48+
49+
function headers(cookie) {
50+
return {
51+
headers: {
52+
Cookie: cookie,
53+
'Content-Type': 'application/json',
54+
},
55+
};
56+
}
57+
58+
function ensureStatus(res, statuses, op) {
59+
const ok = check(res, {
60+
[`${op} status`]: (r) => statuses.includes(r.status),
61+
});
62+
e2eFailed.add(!ok, { op });
63+
if (!ok) {
64+
const snippet = typeof res.body === 'string' ? res.body.slice(0, 300) : '';
65+
throw new Error(`${op} failed status=${res.status}, body=${snippet}`);
66+
}
67+
}
68+
69+
function parseJson(res, op) {
70+
try {
71+
return res.json();
72+
} catch (_e) {
73+
throw new Error(`${op} invalid JSON`);
74+
}
75+
}
76+
77+
function login(email) {
78+
const loginJar = new http.CookieJar();
79+
const res = http.post(
80+
`${BASE_URL}/api/user/login`,
81+
JSON.stringify({ email, password: USER_PASSWORD }),
82+
{
83+
headers: { 'Content-Type': 'application/json' },
84+
jar: loginJar,
85+
tags: { op: 'preview_login', name: 'POST /api/user/login' },
86+
},
87+
);
88+
tLogin.add(res.timings.duration);
89+
ensureStatus(res, [200], 'preview_login');
90+
91+
const jsession = res.cookies.JSESSIONID && res.cookies.JSESSIONID[0];
92+
if (!jsession || !jsession.value) {
93+
throw new Error('preview_login missing JSESSIONID');
94+
}
95+
return `JSESSIONID=${jsession.value}`;
96+
}
97+
98+
function fetchDocs(cookie) {
99+
const res = http.get(
100+
`${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`,
101+
{ headers: { Cookie: cookie }, tags: { op: 'preview_setup_doc_list', name: 'GET /api/document' } },
102+
);
103+
tSetupDocList.add(res.timings.duration);
104+
ensureStatus(res, [200], 'preview_setup_doc_list');
105+
const body = parseJson(res, 'preview_setup_doc_list');
106+
if (!body || !Array.isArray(body.content) || body.content.length === 0) {
107+
throw new Error('preview_setup_doc_list empty content');
108+
}
109+
return body.content.map((doc) => ({ docId: doc.id }));
110+
}
111+
112+
function attachSaveIds(cookie, docs) {
113+
return docs.map((doc) => {
114+
const res = http.get(
115+
`${BASE_URL}/api/document/${doc.docId}/graph`,
116+
{ headers: { Cookie: cookie }, tags: { op: 'preview_setup_graph', name: 'GET /api/document/{docId}/graph' } },
117+
);
118+
tSetupGraph.add(res.timings.duration);
119+
ensureStatus(res, [200], 'preview_setup_graph');
120+
const graph = parseJson(res, 'preview_setup_graph');
121+
const branch = graph.branches && graph.branches.find((item) => item.saveId);
122+
if (!branch) {
123+
throw new Error(`preview_setup_graph missing saveId docId=${doc.docId}`);
124+
}
125+
return { docId: doc.docId, saveId: branch.saveId };
126+
});
127+
}
128+
129+
function buildSaveContent(docId, iteration) {
130+
return [
131+
{
132+
id: `preview-e2e-${docId}-${iteration}-1`,
133+
type: 'paragraph',
134+
props: {},
135+
content: [
136+
{
137+
type: 'text',
138+
text: `preview e2e ${RUN_ID} doc ${docId} iteration ${iteration}`,
139+
styles: {},
140+
},
141+
],
142+
children: [],
143+
},
144+
];
145+
}
146+
147+
function requestSaveUpdate(cookie, target, iteration) {
148+
const res = http.put(
149+
`${BASE_URL}/api/document/${target.docId}/save/${target.saveId}`,
150+
JSON.stringify({ content: buildSaveContent(target.docId, iteration) }),
151+
{ ...headers(cookie), tags: { op: 'preview_save_update', name: 'PUT /api/document/{docId}/save/{saveId}' } },
152+
);
153+
tSaveUpdate.add(res.timings.duration);
154+
ensureStatus(res, [200], 'preview_save_update');
155+
}
156+
157+
function requestDocList(cookie) {
158+
const res = http.get(
159+
`${BASE_URL}/api/document?page=0&size=${DOCS_PER_USER}&sort=updatedAt&order=desc`,
160+
{ headers: { Cookie: cookie }, tags: { op: 'preview_doc_list', name: 'GET /api/document' } },
161+
);
162+
tDocList.add(res.timings.duration);
163+
ensureStatus(res, [200], 'preview_doc_list');
164+
}
165+
166+
export function setup() {
167+
const users = [];
168+
for (let userNo = 1; userNo <= USER_COUNT; userNo += 1) {
169+
const cookie = login(userEmail(userNo));
170+
const docs = attachSaveIds(cookie, fetchDocs(cookie));
171+
users.push({ userNo, cookie, docs });
172+
}
173+
return { users };
174+
}
175+
176+
export function runPreviewE2E(data) {
177+
const userIndex = (exec.vu.idInTest - 1) % data.users.length;
178+
const user = data.users[userIndex];
179+
const iteration = exec.scenario.iterationInTest;
180+
const target = user.docs[(iteration + exec.vu.idInTest - 1) % user.docs.length];
181+
const started = Date.now();
182+
183+
requestSaveUpdate(user.cookie, target, iteration);
184+
requestDocList(user.cookie);
185+
186+
tTotal.add(Date.now() - started);
187+
}
188+
189+
export function handleSummary(data) {
190+
return {
191+
stdout: `\n[preview-e2e] run_id=${RUN_ID}, users=${USER_COUNT}, docs_per_user=${DOCS_PER_USER}, vus=${E2E_VUS}, duration=${E2E_DURATION}\n`,
192+
[`${RESULT_DIR}/preview_e2e_${RUN_ID}.json`]: JSON.stringify(data, null, 2),
193+
};
194+
}

0 commit comments

Comments
 (0)