1
45import http from "k6/http";
46import { check } from "k6";
47import { Trend, Rate } from "k6/metrics";
48
49const tableFirstMs = new Trend("table_first_ms", true);
50const tableScrollMs = new Trend("table_scroll_ms", true);
51const recordOpenMs = new Trend("record_open_ms", true);
52const recordCreateMs = new Trend("record_create_ms", true);
53const createStall = new Rate("create_stall_rate");
54
55const API = __ENV.K6_API_URL || "http://localhost:4000";
56const EMAIL = __ENV.K6_EMAIL || "demo@factory.local";
57const PASSWORD = __ENV.K6_PASSWORD || "Factory!2345";
58const STRICT = (__ENV.K6_STRICT || "1") !== "0";
59const SMOKE = __ENV.K6_PROFILE === "smoke";
60const DB_ID = __ENV.K6_DB_ID || "";
61
62
63
64
65
66const TABLE_SECS = SMOKE ? 60 : 120;
67const CREATE_SECS = SMOKE ? 60 : 120;
68const OPEN_SECS = SMOKE ? 60 : 60;
69
70
71
72
73const CREATE_RPS = SMOKE ? 1 : 100;
74const TABLE_FIRST_VUS = SMOKE ? 1 : 50;
75const TABLE_SCROLL_VUS = SMOKE ? 1 : 100;
76const OPEN_VUS = SMOKE ? 1 : 50;
77
78const PAGE_SIZE = 50;
79
80
81
82
83const CHAIN_PAGES = SMOKE ? 2 : 50;
84
85
86
87
88
89
90
91
92
93let _DATA = null;
94function data() {
95 if (!_DATA) _DATA = resolveData();
96 return _DATA;
97}
98function resolveData() {
99 let res = http.post(`${API}/api/v1/auth/login`, JSON.stringify({ email: EMAIL, password: PASSWORD }), {
100 headers: { "Content-Type": "application/json" },
101 });
102 if (res.status !== 200) {
103 res = http.post(`${API}/api/v1/auth/register`, JSON.stringify({ email: EMAIL, password: PASSWORD, name: "Perf DbLoad" }), {
104 headers: { "Content-Type": "application/json" },
105 });
106 res = http.post(`${API}/api/v1/auth/login`, JSON.stringify({ email: EMAIL, password: PASSWORD }), {
107 headers: { "Content-Type": "application/json" },
108 });
109 }
110 if (res.status !== 200) throw new Error(`auth failed: ${res.status} ${res.body.slice(0, 200)}`);
111 const body = res.json();
112 const token = body.accessToken || body.access_token || body.token;
113 if (!token) throw new Error(`no token in auth response`);
114 if (!DB_ID) throw new Error("K6_DB_ID is empty — run seed-100k.mjs and export its databaseId.");
115
116 const H = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
117
118
119
120 const cursorChain = [];
121 const recordIds = [];
122 let cursor = null;
123 for (let page = 1; page <= CHAIN_PAGES; page++) {
124 const q = { pageSize: PAGE_SIZE };
125 if (cursor !== null) q.cursor = cursor;
126 const r = http.post(`${API}/api/v1/databases/${DB_ID}/query`, JSON.stringify(q), { headers: H });
127 if (r.status !== 200) throw new Error(`query setup page ${page} failed: ${r.status}`);
128 const data = r.json().data || {};
129 const records = data.records || [];
130 if (records.length === 0) break;
131 cursorChain.push(cursor);
132 for (const rec of records) if (rec.id) recordIds.push(rec.id);
133 cursor = data.nextCursor ?? null;
134 if (cursor === null) break;
135 }
136 if (recordIds.length === 0) throw new Error("no records found in the seeded DB — is K6_DB_ID correct?");
137 return { token, cursorChain, recordIds };
138}
139export function setup() {
140
141
142
143 resolveData();
144 return { ok: true };
145}
146
147function H(token) {
148 return { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
149}
150
151export function tableFirst() {
152 const res = http.post(`${API}/api/v1/databases/${DB_ID}/query`, JSON.stringify({ pageSize: PAGE_SIZE }), {
153 headers: H(data().token),
154 });
155 tableFirstMs.add(res.timings.duration);
156 check(res, {
157 "[db] table-first 2xx": (r) => r.status >= 200 && r.status < 300,
158 "[db] table-first has records": (r) => (r.json().data?.records || []).length > 0,
159 });
160}
161
162export function tableScroll() {
163 const d = data();
164
165 const idx = 1 + Math.floor(Math.random() * Math.max(1, d.cursorChain.length - 1));
166 const cursor = d.cursorChain[Math.min(idx, d.cursorChain.length - 1)];
167 const q = { pageSize: PAGE_SIZE };
168 if (cursor !== null && cursor !== undefined) q.cursor = cursor;
169 const res = http.post(`${API}/api/v1/databases/${DB_ID}/query`, JSON.stringify(q), {
170 headers: H(d.token),
171 });
172 tableScrollMs.add(res.timings.duration);
173 check(res, {
174 "[db] table-scroll 2xx": (r) => r.status >= 200 && r.status < 300,
175 "[db] table-scroll has records": (r) => (r.json().data?.records || []).length > 0,
176 });
177}
178
179export function recordOpen() {
180 const t0 = Date.now();
181 const d = data();
182 const rid = d.recordIds[Math.floor(Math.random() * d.recordIds.length)];
183 const rec = http.get(`${API}/api/v1/records/${rid}`, { headers: H(d.token) });
184 if (rec.status >= 200 && rec.status < 300) {
185 const pageId = rec.json().pageId;
186 if (pageId) {
187 const blocks = http.get(`${API}/api/v1/pages/${pageId}/blocks`, { headers: H(d.token) });
188 check(blocks, { "[db] record-open blocks 2xx": (r) => r.status >= 200 && r.status < 300 });
189 }
190 }
191 recordOpenMs.add(Date.now() - t0);
192 check(rec, { "[db] record-open 2xx": (r) => r.status >= 200 && r.status < 300 });
193}
194
195export function recordCreate() {
196 const res = http.post(
197 `${API}/api/v1/databases/${DB_ID}/records`,
198 JSON.stringify({ title: `Perf DbLoad ${__VU}-${__ITER}` }),
199 { headers: H(data().token) }
200 );
201 recordCreateMs.add(res.timings.duration);
202 if (res.timings.duration > 5000) createStall.add(true);
203 else createStall.add(false);
204 check(res, { "[db] record-create 2xx": (r) => r.status >= 200 && r.status < 300 });
205}
206
207const HARD_THRESHOLDS = {
208 table_first_ms: ["p(95)<500"],
209 table_scroll_ms: ["p(95)<500"],
210 record_open_ms: ["p(95)<500"],
211 record_create_ms: ["p(95)<500"],
212
213 create_stall_rate: ["rate<0.001"],
214 http_req_failed: ["rate<0.001"],
215};
216const SOFT_THRESHOLDS = {
217 table_first_ms: ["avg<1e15"],
218 table_scroll_ms: ["avg<1e15"],
219 record_open_ms: ["avg<1e15"],
220 record_create_ms: ["avg<1e15"],
221
222
223
224 create_stall_rate: ["rate<=1"],
225 http_req_failed: ["rate<=1"],
226};
227
228export const options = {
229 scenarios: {
230 "table-first": {
231 executor: "constant-vus",
232 startTime: "0s",
233 duration: `${TABLE_SECS}s`,
234 vus: TABLE_FIRST_VUS,
235 exec: "tableFirst",
236 },
237 "table-scroll": {
238 executor: "constant-vus",
239 startTime: "0s",
240 duration: `${TABLE_SECS}s`,
241 vus: TABLE_SCROLL_VUS,
242 exec: "tableScroll",
243 },
244 "record-open": {
245 executor: "constant-vus",
246 startTime: "0s",
247 duration: `${OPEN_SECS}s`,
248 vus: OPEN_VUS,
249 exec: "recordOpen",
250 },
251
252 "record-create": {
253 executor: "constant-arrival-rate",
254 startTime: "0s",
255 duration: `${CREATE_SECS}s`,
256 preAllocatedVUs: SMOKE ? 5 : 50,
257 maxVUs: SMOKE ? 20 : 200,
258 rate: CREATE_RPS,
259 exec: "recordCreate",
260 },
261 },
262 thresholds: STRICT ? HARD_THRESHOLDS : SOFT_THRESHOLDS,
263 summaryTrendStats: ["avg", "min", "med", "p(90)", "p(95)", "p(99)", "max"],
264};
265