@donkeylabs/server 2.0.19 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,974 @@
1
+ # Load Testing Guide
2
+
3
+ Performance testing for DonkeyLabs applications using k6 and Artillery.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Getting Started with k6](#getting-started-with-k6)
8
+ - [Basic Load Tests](#basic-load-tests)
9
+ - [Advanced Scenarios](#advanced-scenarios)
10
+ - [Testing DonkeyLabs Specifics](#testing-donkeylabs-specifics)
11
+ - [Performance Benchmarks](#performance-benchmarks)
12
+ - [Continuous Load Testing](#continuous-load-testing)
13
+ - [Troubleshooting Performance Issues](#troubleshooting-performance-issues)
14
+
15
+ ---
16
+
17
+ ## Getting Started with k6
18
+
19
+ ### Installation
20
+
21
+ ```bash
22
+ # macOS
23
+ brew install k6
24
+
25
+ # Linux
26
+ curl -s https://packagecloud.io/install/repositories/loadimpact/stable/script.deb.sh | sudo bash
27
+ sudo apt-get install k6
28
+
29
+ # Docker
30
+ docker pull grafana/k6
31
+
32
+ # Verify
33
+ k6 version
34
+ ```
35
+
36
+ ### Your First Test
37
+
38
+ ```javascript
39
+ // load-tests/smoke-test.js
40
+ import http from 'k6/http';
41
+ import { check, sleep } from 'k6';
42
+
43
+ export const options = {
44
+ vus: 10, // Virtual users
45
+ duration: '30s', // Test duration
46
+ thresholds: {
47
+ http_req_duration: ['p(95)<500'], // 95% under 500ms
48
+ http_req_failed: ['rate<0.1'], // Error rate < 10%
49
+ },
50
+ };
51
+
52
+ export default function () {
53
+ const res = http.get('http://localhost:3000/health');
54
+
55
+ check(res, {
56
+ 'status is 200': (r) => r.status === 200,
57
+ 'response time < 500ms': (r) => r.timings.duration < 500,
58
+ });
59
+
60
+ sleep(1);
61
+ }
62
+ ```
63
+
64
+ ```bash
65
+ # Run smoke test
66
+ k6 run load-tests/smoke-test.js
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Basic Load Tests
72
+
73
+ ### 1. Smoke Test (Validate System)
74
+
75
+ ```javascript
76
+ // load-tests/smoke.js
77
+ import http from 'k6/http';
78
+ import { check } from 'k6';
79
+
80
+ export const options = {
81
+ vus: 1,
82
+ iterations: 1,
83
+ };
84
+
85
+ export default function () {
86
+ // Test critical endpoints
87
+ const checks = [
88
+ http.get('http://localhost:3000/health'),
89
+ http.get('http://localhost:3000/users.list'),
90
+ http.post('http://localhost:3000/users.create', {
91
+ email: 'test@example.com',
92
+ name: 'Test User',
93
+ }),
94
+ ];
95
+
96
+ checks.forEach((res, i) => {
97
+ check(res, {
98
+ [`endpoint ${i} status 200`]: (r) => r.status === 200,
99
+ });
100
+ });
101
+ }
102
+ ```
103
+
104
+ ### 2. Load Test (Normal Traffic)
105
+
106
+ ```javascript
107
+ // load-tests/load.js
108
+ import http from 'k6/http';
109
+ import { check, sleep } from 'k6';
110
+
111
+ export const options = {
112
+ stages: [
113
+ { duration: '2m', target: 50 }, // Ramp up
114
+ { duration: '5m', target: 50 }, // Stay at 50
115
+ { duration: '2m', target: 100 }, // Ramp up
116
+ { duration: '5m', target: 100 }, // Stay at 100
117
+ { duration: '2m', target: 0 }, // Ramp down
118
+ ],
119
+ thresholds: {
120
+ http_req_duration: ['p(95)<500'],
121
+ http_req_failed: ['rate<0.1'],
122
+ },
123
+ };
124
+
125
+ const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
126
+
127
+ export default function () {
128
+ // Simulate user flow
129
+
130
+ // 1. List users
131
+ const listRes = http.get(`${BASE_URL}/users.list`);
132
+ check(listRes, {
133
+ 'list status 200': (r) => r.status === 200,
134
+ 'list response time < 500ms': (r) => r.timings.duration < 500,
135
+ });
136
+
137
+ sleep(2);
138
+
139
+ // 2. Get specific user
140
+ const userId = 'user-123'; // In real test, get from list response
141
+ const getRes = http.get(`${BASE_URL}/users.get?id=${userId}`);
142
+ check(getRes, {
143
+ 'get status 200': (r) => r.status === 200,
144
+ });
145
+
146
+ sleep(3);
147
+ }
148
+ ```
149
+
150
+ ### 3. Stress Test (Find Breaking Point)
151
+
152
+ ```javascript
153
+ // load-tests/stress.js
154
+ import http from 'k6/http';
155
+ import { check } from 'k6';
156
+
157
+ export const options = {
158
+ stages: [
159
+ { duration: '2m', target: 100 }, // Below normal
160
+ { duration: '5m', target: 100 }, // Normal
161
+ { duration: '2m', target: 200 }, // Above normal
162
+ { duration: '5m', target: 200 }, // Stress
163
+ { duration: '2m', target: 300 }, // Breaking point
164
+ { duration: '5m', target: 300 }, // Stay there
165
+ { duration: '2m', target: 0 }, // Recovery
166
+ ],
167
+ thresholds: {
168
+ http_req_duration: ['p(95)<1000'],
169
+ },
170
+ };
171
+
172
+ export default function () {
173
+ const res = http.get('http://localhost:3000/users.list');
174
+
175
+ check(res, {
176
+ 'status is 200 or 503': (r) => [200, 503].includes(r.status),
177
+ });
178
+ }
179
+ ```
180
+
181
+ ### 4. Spike Test (Sudden Traffic)
182
+
183
+ ```javascript
184
+ // load-tests/spike.js
185
+ import http from 'k6/http';
186
+ import { check } from 'k6';
187
+
188
+ export const options = {
189
+ stages: [
190
+ { duration: '10s', target: 100 }, // Baseline
191
+ { duration: '1m', target: 100 }, // Stay
192
+ { duration: '10s', target: 1000 }, // Spike!
193
+ { duration: '3m', target: 1000 }, // Stay
194
+ { duration: '10s', target: 100 }, // Drop
195
+ { duration: '3m', target: 100 }, // Recovery
196
+ { duration: '10s', target: 0 }, // Done
197
+ ],
198
+ };
199
+
200
+ export default function () {
201
+ const res = http.get('http://localhost:3000/users.list');
202
+ check(res, {
203
+ 'status is acceptable': (r) => [200, 429, 503].includes(r.status),
204
+ });
205
+ }
206
+ ```
207
+
208
+ ### 5. Soak Test (Endurance)
209
+
210
+ ```javascript
211
+ // load-tests/soak.js
212
+ import http from 'k6/http';
213
+ import { check, sleep } from 'k6';
214
+
215
+ export const options = {
216
+ stages: [
217
+ { duration: '2m', target: 100 }, // Ramp up
218
+ { duration: '4h', target: 100 }, // Stay for 4 hours
219
+ { duration: '2m', target: 0 }, // Ramp down
220
+ ],
221
+ thresholds: {
222
+ http_req_duration: ['p(95)<500'],
223
+ http_req_failed: ['rate<0.1'],
224
+ },
225
+ };
226
+
227
+ export default function () {
228
+ // Mix of operations
229
+ const scenarios = [
230
+ () => http.get('http://localhost:3000/users.list'),
231
+ () => http.get('http://localhost:3000/users.get?id=user-1'),
232
+ () => http.post('http://localhost:3000/users.create', {
233
+ email: `user-${Date.now()}@test.com`,
234
+ name: 'Test',
235
+ }),
236
+ ];
237
+
238
+ const randomScenario = scenarios[Math.floor(Math.random() * scenarios.length)];
239
+ const res = randomScenario();
240
+
241
+ check(res, {
242
+ 'request successful': (r) => r.status === 200,
243
+ });
244
+
245
+ sleep(Math.random() * 2 + 1); // Random sleep 1-3s
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Advanced Scenarios
252
+
253
+ ### Authentication Flow
254
+
255
+ ```javascript
256
+ // load-tests/auth-flow.js
257
+ import http from 'k6/http';
258
+ import { check, sleep } from 'k6';
259
+
260
+ export const options = {
261
+ vus: 50,
262
+ duration: '5m',
263
+ };
264
+
265
+ let authToken = null;
266
+
267
+ export function setup() {
268
+ // Login and get token
269
+ const loginRes = http.post('http://localhost:3000/auth.login', {
270
+ email: 'loadtest@example.com',
271
+ password: 'testpass123',
272
+ });
273
+
274
+ check(loginRes, {
275
+ 'login successful': (r) => r.status === 200,
276
+ });
277
+
278
+ return { token: loginRes.json('token') };
279
+ }
280
+
281
+ export default function (data) {
282
+ const headers = {
283
+ Authorization: `Bearer ${data.token}`,
284
+ 'Content-Type': 'application/json',
285
+ };
286
+
287
+ // Authenticated request
288
+ const res = http.get('http://localhost:3000/users.me', { headers });
289
+
290
+ check(res, {
291
+ 'authenticated request success': (r) => r.status === 200,
292
+ 'has user data': (r) => r.json('id') !== undefined,
293
+ });
294
+
295
+ sleep(1);
296
+ }
297
+ ```
298
+
299
+ ### Data-Driven Tests
300
+
301
+ ```javascript
302
+ // load-tests/data-driven.js
303
+ import http from 'k6/http';
304
+ import { check } from 'k6';
305
+ import { SharedArray } from 'k6/data';
306
+
307
+ // Load test data
308
+ const users = new SharedArray('users', function () {
309
+ return JSON.parse(open('./data/users.json'));
310
+ });
311
+
312
+ export const options = {
313
+ vus: 10,
314
+ iterations: users.length,
315
+ };
316
+
317
+ export default function () {
318
+ const user = users[__ITER];
319
+
320
+ const res = http.post('http://localhost:3000/users.create', {
321
+ email: user.email,
322
+ name: user.name,
323
+ });
324
+
325
+ check(res, {
326
+ 'user created': (r) => r.status === 200 || r.status === 409, // 409 if exists
327
+ });
328
+ }
329
+ ```
330
+
331
+ ### WebSocket Testing
332
+
333
+ ```javascript
334
+ // load-tests/websocket.js
335
+ import ws from 'k6/ws';
336
+ import { check } from 'k6';
337
+
338
+ export const options = {
339
+ vus: 10,
340
+ duration: '1m',
341
+ };
342
+
343
+ export default function () {
344
+ const url = 'ws://localhost:3000/ws';
345
+
346
+ const res = ws.connect(url, null, function (socket) {
347
+ socket.on('open', () => {
348
+ socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
349
+ });
350
+
351
+ socket.on('message', (data) => {
352
+ const msg = JSON.parse(data);
353
+ check(msg, {
354
+ 'received message': () => msg.type !== undefined,
355
+ });
356
+ });
357
+
358
+ socket.setTimeout(function () {
359
+ socket.close();
360
+ }, 30000);
361
+ });
362
+
363
+ check(res, { 'status is 101': (r) => r && r.status === 101 });
364
+ }
365
+ ```
366
+
367
+ ### SSE (Server-Sent Events) Testing
368
+
369
+ ```javascript
370
+ // load-tests/sse.js
371
+ import http from 'k6/http';
372
+ import { check } from 'k6';
373
+
374
+ export const options = {
375
+ vus: 50,
376
+ duration: '2m',
377
+ };
378
+
379
+ export default function () {
380
+ // Connect to SSE endpoint
381
+ const res = http.get('http://localhost:3000/sse?channels=updates', {
382
+ headers: {
383
+ Accept: 'text/event-stream',
384
+ },
385
+ responseType: 'text',
386
+ timeout: '120s',
387
+ });
388
+
389
+ check(res, {
390
+ 'SSE connection established': (r) => r.status === 200,
391
+ 'content-type is event-stream': (r) =>
392
+ r.headers['Content-Type'] === 'text/event-stream',
393
+ });
394
+
395
+ // Parse SSE data
396
+ const events = res.body.split('\n\n');
397
+ check(events, {
398
+ 'received events': (e) => e.length > 0,
399
+ });
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Testing DonkeyLabs Specifics
406
+
407
+ ### Testing with Generated Client
408
+
409
+ ```javascript
410
+ // load-tests/using-client.js
411
+ import http from 'k6/http';
412
+ import { check } from 'k6';
413
+
414
+ // Simulate the API client structure
415
+ class DonkeyLabsClient {
416
+ constructor(baseUrl) {
417
+ this.baseUrl = baseUrl;
418
+ }
419
+
420
+ users = {
421
+ list: () => http.get(`${this.baseUrl}/users.list`),
422
+ get: (id) => http.get(`${this.baseUrl}/users.get?id=${id}`),
423
+ create: (data) =>
424
+ http.post(`${this.baseUrl}/users.create`, JSON.stringify(data), {
425
+ headers: { 'Content-Type': 'application/json' },
426
+ }),
427
+ };
428
+
429
+ orders = {
430
+ list: () => http.get(`${this.baseUrl}/orders.list`),
431
+ create: (data) =>
432
+ http.post(`${this.baseUrl}/orders.create`, JSON.stringify(data), {
433
+ headers: { 'Content-Type': 'application/json' },
434
+ }),
435
+ };
436
+ }
437
+
438
+ const api = new DonkeyLabsClient(__ENV.BASE_URL || 'http://localhost:3000');
439
+
440
+ export const options = {
441
+ vus: 100,
442
+ duration: '5m',
443
+ };
444
+
445
+ export default function () {
446
+ // Create a user
447
+ const createRes = api.users.create({
448
+ email: `user-${__VU}-${__ITER}@test.com`,
449
+ name: 'Load Test User',
450
+ });
451
+
452
+ check(createRes, {
453
+ 'user created': (r) => r.status === 200,
454
+ });
455
+
456
+ if (createRes.status === 200) {
457
+ const userId = createRes.json('id');
458
+
459
+ // Get the user
460
+ const getRes = api.users.get(userId);
461
+ check(getRes, {
462
+ 'user retrieved': (r) => r.status === 200,
463
+ });
464
+
465
+ // Create an order for the user
466
+ const orderRes = api.orders.create({
467
+ userId,
468
+ items: [{ productId: 'prod-1', quantity: 2 }],
469
+ });
470
+
471
+ check(orderRes, {
472
+ 'order created': (r) => r.status === 200,
473
+ });
474
+ }
475
+ }
476
+ ```
477
+
478
+ ### Plugin Service Testing
479
+
480
+ ```javascript
481
+ // load-tests/plugin-test.js
482
+ import http from 'k6/http';
483
+ import { check } from 'k6';
484
+
485
+ // Test specific plugin functionality
486
+ export const options = {
487
+ scenarios: {
488
+ cache_test: {
489
+ executor: 'constant-vus',
490
+ vus: 50,
491
+ duration: '2m',
492
+ exec: 'cacheTest',
493
+ },
494
+ rate_limit_test: {
495
+ executor: 'ramping-vus',
496
+ startVUs: 0,
497
+ stages: [
498
+ { duration: '1m', target: 100 },
499
+ { duration: '1m', target: 100 },
500
+ { duration: '1m', target: 200 },
501
+ ],
502
+ exec: 'rateLimitTest',
503
+ },
504
+ },
505
+ };
506
+
507
+ export function cacheTest() {
508
+ // Test cache hit performance
509
+ const res = http.get('http://localhost:3000/users.list');
510
+
511
+ check(res, {
512
+ 'cache response fast': (r) => r.timings.duration < 50,
513
+ 'status 200': (r) => r.status === 200,
514
+ });
515
+ }
516
+
517
+ export function rateLimitTest() {
518
+ // Test rate limiting
519
+ const res = http.get('http://localhost:3000/api.heavy');
520
+
521
+ check(res, {
522
+ 'rate limited or success': (r) =>
523
+ [200, 429].includes(r.status),
524
+ });
525
+ }
526
+ ```
527
+
528
+ ### Database Load Testing
529
+
530
+ ```javascript
531
+ // load-tests/db-heavy.js
532
+ import http from 'k6/http';
533
+ import { check } from 'k6';
534
+ import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
535
+
536
+ export const options = {
537
+ vus: 20,
538
+ duration: '5m',
539
+ thresholds: {
540
+ http_req_duration: ['p(95)<1000'], // DB ops can be slower
541
+ },
542
+ };
543
+
544
+ export default function () {
545
+ // Heavy database operations
546
+
547
+ // Complex query with joins
548
+ const reportRes = http.post(
549
+ 'http://localhost:3000/reports.generate',
550
+ JSON.stringify({
551
+ type: 'user_activity',
552
+ dateRange: {
553
+ start: '2024-01-01',
554
+ end: '2024-12-31',
555
+ },
556
+ }),
557
+ { headers: { 'Content-Type': 'application/json' } }
558
+ );
559
+
560
+ check(reportRes, {
561
+ 'report generated': (r) => r.status === 200,
562
+ 'report not too slow': (r) => r.timings.duration < 5000,
563
+ });
564
+
565
+ // Bulk insert operation
566
+ const bulkRes = http.post(
567
+ 'http://localhost:3000/users.bulkCreate',
568
+ JSON.stringify({
569
+ users: Array.from({ length: 10 }, (_, i) => ({
570
+ email: `bulk-${__VU}-${__ITER}-${i}@test.com`,
571
+ name: `User ${i}`,
572
+ })),
573
+ }),
574
+ { headers: { 'Content-Type': 'application/json' } }
575
+ );
576
+
577
+ check(bulkRes, {
578
+ 'bulk insert succeeded': (r) => r.status === 200,
579
+ });
580
+ }
581
+ ```
582
+
583
+ ---
584
+
585
+ ## Performance Benchmarks
586
+
587
+ ### Establishing Baselines
588
+
589
+ ```javascript
590
+ // load-tests/benchmark.js
591
+ import http from 'k6/http';
592
+ import { check, group } from 'k6';
593
+
594
+ export const options = {
595
+ vus: 1,
596
+ iterations: 100,
597
+ thresholds: {
598
+ 'http_req_duration{group:::health}': ['avg<10'],
599
+ 'http_req_duration{group:::list}': ['avg<100'],
600
+ 'http_req_duration{group:::create}': ['avg<200'],
601
+ },
602
+ };
603
+
604
+ export default function () {
605
+ group('health', () => {
606
+ const res = http.get('http://localhost:3000/health');
607
+ check(res, {
608
+ 'health check': (r) => r.status === 200,
609
+ });
610
+ });
611
+
612
+ group('list', () => {
613
+ const res = http.get('http://localhost:3000/users.list');
614
+ check(res, {
615
+ 'list users': (r) => r.status === 200,
616
+ });
617
+ });
618
+
619
+ group('create', () => {
620
+ const res = http.post(
621
+ 'http://localhost:3000/users.create',
622
+ JSON.stringify({
623
+ email: `bench-${Date.now()}@test.com`,
624
+ name: 'Benchmark',
625
+ }),
626
+ { headers: { 'Content-Type': 'application/json' } }
627
+ );
628
+ check(res, {
629
+ 'create user': (r) => r.status === 200,
630
+ });
631
+ });
632
+ }
633
+ ```
634
+
635
+ ### Run Benchmarks
636
+
637
+ ```bash
638
+ # Run and output to JSON for analysis
639
+ k6 run --out json=benchmark-results.json load-tests/benchmark.js
640
+
641
+ # Run with custom thresholds
642
+ k6 run -e THRESHOLD_P95=200 load-tests/benchmark.js
643
+
644
+ # Compare against previous run
645
+ k6 run --out influxdb=http://localhost:8086/k6 load-tests/benchmark.js
646
+ ```
647
+
648
+ ### Performance Regression Testing
649
+
650
+ ```javascript
651
+ // load-tests/regression.js
652
+ import http from 'k6/http';
653
+ import { check, fail } from 'k6';
654
+
655
+ // Baseline metrics from previous runs
656
+ const BASELINE = {
657
+ health_p95: 10,
658
+ list_p95: 100,
659
+ create_p95: 200,
660
+ };
661
+
662
+ // Allow 20% regression
663
+ const THRESHOLD = 1.2;
664
+
665
+ export const options = {
666
+ vus: 50,
667
+ duration: '2m',
668
+ };
669
+
670
+ export default function () {
671
+ const results = {
672
+ health: [],
673
+ list: [],
674
+ create: [],
675
+ };
676
+
677
+ // Collect metrics
678
+ results.health.push(http.get('http://localhost:3000/health').timings.duration);
679
+ results.list.push(http.get('http://localhost:3000/users.list').timings.duration);
680
+ results.create.push(
681
+ http.post('http://localhost:3000/users.create', {
682
+ email: `reg-${Date.now()}@test.com`,
683
+ name: 'Test',
684
+ }).timings.duration
685
+ );
686
+
687
+ // Check for regression (simplified, normally done in handleSummary)
688
+ }
689
+
690
+ export function handleSummary(data) {
691
+ const checks = {
692
+ health: data.metrics.http_req_duration.percentiles['95'] < BASELINE.health_p95 * THRESHOLD,
693
+ list: data.metrics.http_req_duration.percentiles['95'] < BASELINE.list_p95 * THRESHOLD,
694
+ create: data.metrics.http_req_duration.percentiles['95'] < BASELINE.create_p95 * THRESHOLD,
695
+ };
696
+
697
+ if (!Object.values(checks).every(Boolean)) {
698
+ return {
699
+ stdout: JSON.stringify({
700
+ status: 'FAIL',
701
+ message: 'Performance regression detected',
702
+ checks,
703
+ }),
704
+ };
705
+ }
706
+
707
+ return {
708
+ stdout: JSON.stringify({
709
+ status: 'PASS',
710
+ message: 'No regression detected',
711
+ checks,
712
+ }),
713
+ };
714
+ }
715
+ ```
716
+
717
+ ---
718
+
719
+ ## Continuous Load Testing
720
+
721
+ ### GitHub Actions Integration
722
+
723
+ ```yaml
724
+ # .github/workflows/load-test.yml
725
+ name: Load Test
726
+
727
+ on:
728
+ schedule:
729
+ - cron: '0 2 * * *' # Daily at 2 AM
730
+ workflow_dispatch:
731
+
732
+ jobs:
733
+ load-test:
734
+ runs-on: ubuntu-latest
735
+
736
+ steps:
737
+ - uses: actions/checkout@v3
738
+
739
+ - name: Setup k6
740
+ run: |
741
+ curl -s https://packagecloud.io/install/repositories/loadimpact/stable/script.deb.sh | sudo bash
742
+ sudo apt-get install k6
743
+
744
+ - name: Start test server
745
+ run: |
746
+ docker-compose up -d
747
+ sleep 10 # Wait for startup
748
+
749
+ - name: Run smoke test
750
+ run: k6 run load-tests/smoke.js
751
+
752
+ - name: Run load test
753
+ run: k6 run --out json=results.json load-tests/load.js
754
+
755
+ - name: Upload results
756
+ uses: actions/upload-artifact@v3
757
+ with:
758
+ name: load-test-results
759
+ path: results.json
760
+
761
+ - name: Stop test server
762
+ run: docker-compose down
763
+ ```
764
+
765
+ ### Performance Budget
766
+
767
+ ```javascript
768
+ // load-tests/budget.js
769
+ import http from 'k6/http';
770
+ import { check } from 'k6';
771
+
772
+ // Performance budget
773
+ const BUDGET = {
774
+ requests: {
775
+ count: 100000, // Max 100k requests
776
+ errorRate: 0.01, // Max 1% errors
777
+ },
778
+ timing: {
779
+ median: 100, // Median < 100ms
780
+ p95: 500, // 95th < 500ms
781
+ p99: 1000, // 99th < 1s
782
+ },
783
+ data: {
784
+ download: 1000000, // Max 1MB download per request
785
+ },
786
+ };
787
+
788
+ export const options = {
789
+ vus: 100,
790
+ duration: '5m',
791
+ thresholds: {
792
+ http_req_duration: [
793
+ `med<${BUDGET.timing.median}`,
794
+ `p(95)<${BUDGET.timing.p95}`,
795
+ `p(99)<${BUDGET.timing.p99}`,
796
+ ],
797
+ http_req_failed: [`rate<${BUDGET.requests.errorRate}`],
798
+ data_received: [`avg<${BUDGET.data.download}`],
799
+ },
800
+ };
801
+
802
+ export default function () {
803
+ const res = http.get('http://localhost:3000/users.list');
804
+
805
+ check(res, {
806
+ 'response time within budget': (r) => r.timings.duration < BUDGET.timing.p95,
807
+ });
808
+ }
809
+ ```
810
+
811
+ ---
812
+
813
+ ## Troubleshooting Performance Issues
814
+
815
+ ### Common Issues
816
+
817
+ **1. High Response Times**
818
+
819
+ ```javascript
820
+ // Debug slow requests
821
+ import http from 'k6/http';
822
+ import { check } from 'k6';
823
+ import { Counter } from 'k6/metrics';
824
+
825
+ const slowRequests = new Counter('slow_requests');
826
+
827
+ export default function () {
828
+ const start = Date.now();
829
+ const res = http.get('http://localhost:3000/users.list');
830
+ const duration = Date.now() - start;
831
+
832
+ if (duration > 1000) {
833
+ slowRequests.add(1);
834
+ console.log(`Slow request: ${duration}ms - ${res.url}`);
835
+ }
836
+
837
+ check(res, {
838
+ 'status 200': (r) => r.status === 200,
839
+ });
840
+ }
841
+ ```
842
+
843
+ **2. Memory Leaks**
844
+
845
+ ```javascript
846
+ // Monitor memory over time
847
+ import exec from 'k6/execution';
848
+
849
+ export const options = {
850
+ vus: 50,
851
+ duration: '30m', // Long test to detect leaks
852
+ };
853
+
854
+ export function setup() {
855
+ return { startTime: Date.now() };
856
+ }
857
+
858
+ export default function (data) {
859
+ // Run normal test
860
+ http.get('http://localhost:3000/users.list');
861
+
862
+ // Log progress
863
+ if (__ITER % 1000 === 0) {
864
+ const elapsed = (Date.now() - data.startTime) / 1000 / 60;
865
+ console.log(`Running for ${elapsed.toFixed(1)} minutes, iteration ${__ITER}`);
866
+ }
867
+ }
868
+ ```
869
+
870
+ **3. Database Connection Issues**
871
+
872
+ ```javascript
873
+ // Test connection pool exhaustion
874
+ export const options = {
875
+ vus: 200, // High concurrency to test pool
876
+ duration: '2m',
877
+ };
878
+
879
+ export default function () {
880
+ // Rapid sequential requests
881
+ for (let i = 0; i < 10; i++) {
882
+ const res = http.get('http://localhost:3000/users.list');
883
+
884
+ check(res, {
885
+ 'no connection errors': (r) => r.status !== 503,
886
+ });
887
+ }
888
+ }
889
+ ```
890
+
891
+ ### Analysis Tools
892
+
893
+ ```bash
894
+ # Generate HTML report
895
+ k6 run --out html=report.html load-tests/load.js
896
+
897
+ # Export to InfluxDB for Grafana
898
+ k6 run --out influxdb=http://localhost:8086/k6 load-tests/load.js
899
+
900
+ # Export to Prometheus
901
+ k6 run --out experimental-prometheus-rw load-tests/load.js
902
+
903
+ # Compare runs
904
+ k6 compare run1.json run2.json
905
+ ```
906
+
907
+ ### Interpreting Results
908
+
909
+ **Good Performance:**
910
+ ```
911
+ http_req_duration..............: avg=45ms min=10ms med=40ms max=150ms p(90)=80ms p(95)=100ms
912
+ http_req_failed................: 0.00%
913
+ http_reqs......................: 50000 1000/s
914
+ ```
915
+
916
+ **Performance Issues:**
917
+ ```
918
+ http_req_duration..............: avg=500ms min=50ms med=450ms max=3000ms p(90)=1200ms p(95)=2000ms
919
+ http_req_failed................: 5.00%
920
+ http_reqs......................: 5000 100/s ← Low throughput
921
+ ```
922
+
923
+ **Actions for Bad Performance:**
924
+ 1. Check database connection pool
925
+ 2. Review slow query logs
926
+ 3. Check for N+1 queries
927
+ 4. Verify caching is working
928
+ 5. Monitor server resources (CPU, memory)
929
+ 6. Check for blocking operations
930
+
931
+ ---
932
+
933
+ ## Quick Reference
934
+
935
+ ### Run Commands
936
+
937
+ ```bash
938
+ # Basic test
939
+ k6 run load-tests/smoke.js
940
+
941
+ # With environment variables
942
+ k6 run -e BASE_URL=https://api.example.com load-tests/load.js
943
+
944
+ # Cloud execution
945
+ k6 cloud run load-tests/load.js
946
+
947
+ # With custom options
948
+ k6 run --vus 100 --duration 5m load-tests/load.js
949
+
950
+ # Verbose output
951
+ k6 run --verbose load-tests/smoke.js
952
+ ```
953
+
954
+ ### Key Metrics
955
+
956
+ | Metric | Good | Warning | Bad |
957
+ |--------|------|---------|-----|
958
+ | http_req_duration (p95) | < 200ms | 200-500ms | > 500ms |
959
+ | http_req_failed | < 0.1% | 0.1-1% | > 1% |
960
+ | http_reqs (throughput) | > 1000/s | 100-1000/s | < 100/s |
961
+ | vus | Scalable | Limited by resources | System overloaded |
962
+
963
+ ### Testing Checklist
964
+
965
+ Before production:
966
+ - [ ] Smoke test passes
967
+ - [ ] Load test meets performance budget
968
+ - [ ] Stress test identifies breaking point
969
+ - [ ] Spike test handles sudden traffic
970
+ - [ ] Soak test stable for 4+ hours
971
+ - [ ] All error rates < 1%
972
+ - [ ] Database handles concurrent connections
973
+ - [ ] Memory usage stable over time
974
+ - [ ] Cache hit rates acceptable