Skip to content

Commit ba26b20

Browse files
committed
vibailtu kuormitusskripti, ohjeet skriptissä
1 parent 3341020 commit ba26b20

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed

backend/scripts/loadtest.js

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Load testing script for Finlex API
4+
*
5+
* Usage:
6+
* node backend/scripts/loadtest.js
7+
* node backend/scripts/loadtest.js --url https://custom-url.fi --concurrent 50 --duration 60
8+
*/
9+
10+
import axios from 'axios';
11+
12+
// Configuration
13+
const CONFIG = {
14+
baseUrl: 'https://finlex.ext.ocp-prod-0.k8s.it.helsinki.fi',
15+
concurrentRequests: 20,
16+
durationSeconds: 30,
17+
requestTimeout: 5000,
18+
};
19+
20+
// Sample search queries (mix of different types)
21+
const SEARCH_QUERIES = [
22+
{ q: 'luonnonsuojelulaki', language: 'fin', type: 'statute' },
23+
{ q: 'tupakka', language: 'fin', type: 'statute' },
24+
{ q: 'työaika', language: 'fin', type: 'statute' },
25+
{ q: 'perintökaari', language: 'fin', type: 'statute' },
26+
{ q: 'rikoslaki', language: 'fin', type: 'statute' },
27+
{ q: '1996/734', language: 'fin', type: 'statute' },
28+
{ q: '2023/527', language: 'fin', type: 'statute' },
29+
{ q: 'vakuutus', language: 'swe', type: 'statute' },
30+
{ q: 'arbete', language: 'swe', type: 'statute' },
31+
{ q: 'vahingonkorvaus', language: 'fin', type: 'judgment' },
32+
{ q: 'työsopimus', language: 'fin', type: 'judgment' },
33+
{ q: 'kiinteistö', language: 'fin', type: 'judgment' },
34+
];
35+
36+
function parseArgs() {
37+
const args = process.argv.slice(2);
38+
const config = { ...CONFIG };
39+
40+
for (let i = 0; i < args.length; i++) {
41+
const arg = args[i];
42+
if (arg === '--url') config.baseUrl = args[++i];
43+
else if (arg === '--concurrent') config.concurrentRequests = parseInt(args[++i]);
44+
else if (arg === '--duration') config.durationSeconds = parseInt(args[++i]);
45+
else if (arg === '--timeout') config.requestTimeout = parseInt(args[++i]);
46+
else if (arg === '--help') {
47+
console.log(`
48+
Load Testing Script for Finlex API
49+
50+
Usage:
51+
node backend/scripts/loadtest.js [options]
52+
53+
Options:
54+
--url <url> Base URL (default: ${CONFIG.baseUrl})
55+
--concurrent <n> Concurrent requests (default: ${CONFIG.concurrentRequests})
56+
--duration <sec> Test duration in seconds (default: ${CONFIG.durationSeconds})
57+
--timeout <ms> Request timeout in ms (default: ${CONFIG.requestTimeout})
58+
--help Show this help message
59+
60+
Examples:
61+
node backend/scripts/loadtest.js
62+
node backend/scripts/loadtest.js --concurrent 50 --duration 60
63+
node backend/scripts/loadtest.js --url https://finlex.ext.ocp-prod-0.k8s.it.helsinki.fi
64+
`);
65+
process.exit(0);
66+
}
67+
}
68+
69+
return config;
70+
}
71+
72+
function buildUrl(base, query) {
73+
const { q, language, type, level } = query;
74+
const params = new URLSearchParams({ q, language });
75+
if (level) params.append('level', level);
76+
return `${base}/api/${type}/search?${params.toString()}`;
77+
}
78+
79+
function getRandomQuery() {
80+
return SEARCH_QUERIES[Math.floor(Math.random() * SEARCH_QUERIES.length)];
81+
}
82+
83+
class LoadTester {
84+
constructor(config) {
85+
this.config = config;
86+
this.stats = {
87+
totalRequests: 0,
88+
successfulRequests: 0,
89+
failedRequests: 0,
90+
totalResponseTime: 0,
91+
responseTimes: [],
92+
errors: {},
93+
statusCodes: {},
94+
};
95+
this.startTime = null;
96+
this.endTime = null;
97+
this.isRunning = false;
98+
}
99+
100+
async makeRequest() {
101+
const query = getRandomQuery();
102+
const url = buildUrl(this.config.baseUrl, query);
103+
const startTime = Date.now();
104+
105+
try {
106+
const response = await axios.get(url, {
107+
timeout: this.config.requestTimeout,
108+
validateStatus: () => true, // Accept all status codes
109+
});
110+
111+
const responseTime = Date.now() - startTime;
112+
this.stats.totalRequests++;
113+
this.stats.totalResponseTime += responseTime;
114+
this.stats.responseTimes.push(responseTime);
115+
116+
const statusCode = response.status;
117+
this.stats.statusCodes[statusCode] = (this.stats.statusCodes[statusCode] || 0) + 1;
118+
119+
if (statusCode >= 200 && statusCode < 500) {
120+
this.stats.successfulRequests++;
121+
} else {
122+
this.stats.failedRequests++;
123+
}
124+
125+
return { success: true, responseTime, statusCode };
126+
} catch (error) {
127+
const responseTime = Date.now() - startTime;
128+
this.stats.totalRequests++;
129+
this.stats.failedRequests++;
130+
this.stats.responseTimes.push(responseTime);
131+
132+
const errorType = error.code || error.message || 'unknown';
133+
this.stats.errors[errorType] = (this.stats.errors[errorType] || 0) + 1;
134+
135+
return { success: false, responseTime, error: errorType };
136+
}
137+
}
138+
139+
async worker() {
140+
while (this.isRunning) {
141+
await this.makeRequest();
142+
}
143+
}
144+
145+
printProgress() {
146+
const elapsed = (Date.now() - this.startTime) / 1000;
147+
const avgResponseTime = this.stats.totalResponseTime / this.stats.totalRequests || 0;
148+
const requestsPerSecond = this.stats.totalRequests / elapsed;
149+
150+
process.stdout.write(`\r` +
151+
`Time: ${elapsed.toFixed(1)}s | ` +
152+
`Requests: ${this.stats.totalRequests} | ` +
153+
`Success: ${this.stats.successfulRequests} | ` +
154+
`Failed: ${this.stats.failedRequests} | ` +
155+
`Avg: ${avgResponseTime.toFixed(0)}ms | ` +
156+
`RPS: ${requestsPerSecond.toFixed(1)}`
157+
);
158+
}
159+
160+
printResults() {
161+
console.log('\n\n' + '='.repeat(60));
162+
console.log('LOAD TEST RESULTS');
163+
console.log('='.repeat(60));
164+
165+
const duration = (this.endTime - this.startTime) / 1000;
166+
const avgResponseTime = this.stats.totalResponseTime / this.stats.totalRequests || 0;
167+
const requestsPerSecond = this.stats.totalRequests / duration;
168+
const successRate = (this.stats.successfulRequests / this.stats.totalRequests * 100).toFixed(2);
169+
170+
// Sort response times for percentile calculations
171+
const sorted = this.stats.responseTimes.sort((a, b) => a - b);
172+
const p50 = sorted[Math.floor(sorted.length * 0.50)] || 0;
173+
const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
174+
const p99 = sorted[Math.floor(sorted.length * 0.99)] || 0;
175+
const min = sorted[0] || 0;
176+
const max = sorted[sorted.length - 1] || 0;
177+
178+
console.log(`\nGeneral:`);
179+
console.log(` Duration: ${duration.toFixed(2)}s`);
180+
console.log(` Total Requests: ${this.stats.totalRequests}`);
181+
console.log(` Successful: ${this.stats.successfulRequests}`);
182+
console.log(` Failed: ${this.stats.failedRequests}`);
183+
console.log(` Success Rate: ${successRate}%`);
184+
console.log(` Requests per Second: ${requestsPerSecond.toFixed(2)}`);
185+
186+
console.log(`\nResponse Times (ms):`);
187+
console.log(` Average: ${avgResponseTime.toFixed(2)}`);
188+
console.log(` Min: ${min}`);
189+
console.log(` Max: ${max}`);
190+
console.log(` p50 (median): ${p50}`);
191+
console.log(` p95: ${p95}`);
192+
console.log(` p99: ${p99}`);
193+
194+
if (Object.keys(this.stats.statusCodes).length > 0) {
195+
console.log(`\nStatus Codes:`);
196+
for (const [code, count] of Object.entries(this.stats.statusCodes).sort()) {
197+
console.log(` ${code}:${' '.repeat(20 - code.length)}${count}`);
198+
}
199+
}
200+
201+
if (Object.keys(this.stats.errors).length > 0) {
202+
console.log(`\nErrors:`);
203+
for (const [error, count] of Object.entries(this.stats.errors)) {
204+
console.log(` ${error}:${' '.repeat(20 - error.length)}${count}`);
205+
}
206+
}
207+
208+
console.log('\n' + '='.repeat(60) + '\n');
209+
}
210+
211+
async run() {
212+
console.log('Starting load test...');
213+
console.log(`URL: ${this.config.baseUrl}`);
214+
console.log(`Concurrent requests: ${this.config.concurrentRequests}`);
215+
console.log(`Duration: ${this.config.durationSeconds}s`);
216+
console.log(`Request timeout: ${this.config.requestTimeout}ms`);
217+
console.log(`\nRunning...\n`);
218+
219+
this.isRunning = true;
220+
this.startTime = Date.now();
221+
222+
// Start workers
223+
const workers = Array(this.config.concurrentRequests)
224+
.fill()
225+
.map(() => this.worker());
226+
227+
// Progress updates
228+
const progressInterval = setInterval(() => this.printProgress(), 1000);
229+
230+
// Stop after duration
231+
setTimeout(() => {
232+
this.isRunning = false;
233+
clearInterval(progressInterval);
234+
}, this.config.durationSeconds * 1000);
235+
236+
// Wait for all workers to finish
237+
await Promise.all(workers);
238+
this.endTime = Date.now();
239+
240+
this.printResults();
241+
}
242+
}
243+
244+
// Main execution
245+
(async () => {
246+
const config = parseArgs();
247+
const tester = new LoadTester(config);
248+
249+
try {
250+
await tester.run();
251+
} catch (error) {
252+
console.error('\nLoad test failed:', error.message);
253+
process.exit(1);
254+
}
255+
})();

0 commit comments

Comments
 (0)