@dismissible/nestjs-api 0.0.2-canary.8976e84.0 → 0.0.2-canary.c91edbc.0

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.

Potentially problematic release.


This version of @dismissible/nestjs-api might be problematic. Click here for more details.

@@ -0,0 +1,855 @@
1
+ import autocannon from 'autocannon';
2
+ import { URL } from 'url';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ interface PerformanceConfig {
7
+ createCount: number;
8
+ getCount: number;
9
+ apiUrl: string;
10
+ connections: number;
11
+ duration: number;
12
+ }
13
+
14
+ interface TestResult {
15
+ phase: string;
16
+ requests: {
17
+ total: number;
18
+ average: number;
19
+ min: number;
20
+ max: number;
21
+ };
22
+ latency: {
23
+ average: number;
24
+ min: number;
25
+ max: number;
26
+ p50: number;
27
+ p90: number;
28
+ p99: number;
29
+ p99_9: number;
30
+ };
31
+ throughput: {
32
+ average: number;
33
+ min: number;
34
+ max: number;
35
+ };
36
+ errors: number;
37
+ timeouts: number;
38
+ duration: number;
39
+ }
40
+
41
+ type RequestConfig = { method: string; path: string };
42
+ type SetRequestsFn = (requests: RequestConfig[]) => void;
43
+
44
+ interface AutocannonClient {
45
+ setRequests: SetRequestsFn;
46
+ }
47
+
48
+ function createPathCycler(paths: string[]): (client: AutocannonClient) => void {
49
+ let pathCounter = 0;
50
+
51
+ return (client: AutocannonClient) => {
52
+ let connectionIndex = pathCounter++;
53
+
54
+ client.setRequests([{ method: 'GET', path: paths[connectionIndex % paths.length] }]);
55
+
56
+ const originalSetRequests = client.setRequests.bind(client);
57
+ client.setRequests = function (_requests: RequestConfig[]) {
58
+ connectionIndex = (connectionIndex + 1) % paths.length;
59
+ return originalSetRequests([{ method: 'GET', path: paths[connectionIndex] }]);
60
+ };
61
+ };
62
+ }
63
+
64
+ function getConfig(): PerformanceConfig {
65
+ return {
66
+ createCount: parseInt(process.env.PERF_CREATE_COUNT || '100', 10),
67
+ getCount: parseInt(process.env.PERF_GET_COUNT || '100', 10),
68
+ apiUrl: process.env.PERF_API_URL || 'http://localhost:3001',
69
+ connections: parseInt(process.env.PERF_CONNECTIONS || '10', 10),
70
+ duration: parseInt(process.env.PERF_DURATION || '10', 10),
71
+ };
72
+ }
73
+
74
+ function getOutputPath(): string | null {
75
+ return process.env.PERF_OUTPUT_PATH || null;
76
+ }
77
+
78
+ function formatNumber(num: number): string {
79
+ return num.toLocaleString('en-US', {
80
+ minimumFractionDigits: 2,
81
+ maximumFractionDigits: 2,
82
+ });
83
+ }
84
+
85
+ function formatBytes(bytes: number): string {
86
+ const units = ['B', 'KB', 'MB', 'GB'];
87
+ let size = bytes;
88
+ let unitIndex = 0;
89
+
90
+ while (size >= 1024 && unitIndex < units.length - 1) {
91
+ size /= 1024;
92
+ unitIndex++;
93
+ }
94
+
95
+ return `${formatNumber(size)} ${units[unitIndex]}`;
96
+ }
97
+
98
+ function formatDuration(ms: number): string {
99
+ if (ms < 1000) {
100
+ return `${formatNumber(ms)} ms`;
101
+ }
102
+ return `${formatNumber(ms / 1000)} s`;
103
+ }
104
+
105
+ function printResults(result: TestResult): void {
106
+ console.log('\n' + '='.repeat(80));
107
+ console.log(`Phase: ${result.phase}`);
108
+ console.log('='.repeat(80));
109
+ console.log(`Duration: ${formatDuration(result.duration)}`);
110
+ console.log('\nšŸ“Š Requests:');
111
+ console.log(` Total: ${result.requests.total.toLocaleString()}`);
112
+ console.log(` Average: ${formatNumber(result.requests.average)} req/s`);
113
+ console.log(` Min: ${formatNumber(result.requests.min)} req/s`);
114
+ console.log(` Max: ${formatNumber(result.requests.max)} req/s`);
115
+
116
+ console.log('\nā±ļø Latency:');
117
+ console.log(` Average: ${formatNumber(result.latency.average)} ms`);
118
+ console.log(` Min: ${formatNumber(result.latency.min)} ms`);
119
+ console.log(` Max: ${formatNumber(result.latency.max)} ms`);
120
+ console.log(` p50: ${formatNumber(result.latency.p50)} ms`);
121
+ console.log(` p90: ${formatNumber(result.latency.p90)} ms`);
122
+ console.log(` p99: ${formatNumber(result.latency.p99)} ms`);
123
+ console.log(` p99.9: ${formatNumber(result.latency.p99_9)} ms`);
124
+
125
+ console.log('\nšŸ“ˆ Throughput:');
126
+ console.log(` Average: ${formatBytes(result.throughput.average)}/s`);
127
+ console.log(` Min: ${formatBytes(result.throughput.min)}/s`);
128
+ console.log(` Max: ${formatBytes(result.throughput.max)}/s`);
129
+
130
+ if (result.errors > 0 || result.timeouts > 0) {
131
+ console.log('\nāš ļø Errors:');
132
+ if (result.errors > 0) {
133
+ console.log(` Errors: ${result.errors.toLocaleString()}`);
134
+ }
135
+ if (result.timeouts > 0) {
136
+ console.log(` Timeouts: ${result.timeouts.toLocaleString()}`);
137
+ }
138
+ }
139
+
140
+ console.log('='.repeat(80) + '\n');
141
+ }
142
+
143
+ function generateHTMLReport(results: TestResult[], config: PerformanceConfig): string {
144
+ const timestamp = new Date().toISOString();
145
+ const createResult = results.find((r) => r.phase === 'Create Items');
146
+ const getResult = results.find((r) => r.phase === 'Get Items');
147
+
148
+ return `<!DOCTYPE html>
149
+ <html lang="en">
150
+ <head>
151
+ <meta charset="UTF-8">
152
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
+ <title>Performance Test Report</title>
154
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
155
+ <style>
156
+ * {
157
+ margin: 0;
158
+ padding: 0;
159
+ box-sizing: border-box;
160
+ }
161
+
162
+ body {
163
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
164
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
165
+ padding: 20px;
166
+ min-height: 100vh;
167
+ }
168
+
169
+ .container {
170
+ max-width: 1400px;
171
+ margin: 0 auto;
172
+ background: white;
173
+ border-radius: 12px;
174
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
175
+ overflow: hidden;
176
+ }
177
+
178
+ .header {
179
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
180
+ color: white;
181
+ padding: 40px;
182
+ text-align: center;
183
+ }
184
+
185
+ .header h1 {
186
+ font-size: 2.5em;
187
+ margin-bottom: 10px;
188
+ }
189
+
190
+ .header .timestamp {
191
+ opacity: 0.9;
192
+ font-size: 0.9em;
193
+ }
194
+
195
+ .config-section {
196
+ background: #f8f9fa;
197
+ padding: 30px 40px;
198
+ border-bottom: 1px solid #e9ecef;
199
+ }
200
+
201
+ .config-section h2 {
202
+ color: #495057;
203
+ margin-bottom: 20px;
204
+ font-size: 1.5em;
205
+ }
206
+
207
+ .config-grid {
208
+ display: grid;
209
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
210
+ gap: 15px;
211
+ }
212
+
213
+ .config-item {
214
+ background: white;
215
+ padding: 15px;
216
+ border-radius: 8px;
217
+ border: 1px solid #e9ecef;
218
+ }
219
+
220
+ .config-item label {
221
+ display: block;
222
+ font-size: 0.85em;
223
+ color: #6c757d;
224
+ margin-bottom: 5px;
225
+ text-transform: uppercase;
226
+ letter-spacing: 0.5px;
227
+ }
228
+
229
+ .config-item value {
230
+ display: block;
231
+ font-size: 1.2em;
232
+ font-weight: 600;
233
+ color: #212529;
234
+ }
235
+
236
+ .phases {
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
239
+ gap: 30px;
240
+ padding: 40px;
241
+ }
242
+
243
+ .phase-card {
244
+ background: #f8f9fa;
245
+ border-radius: 12px;
246
+ padding: 30px;
247
+ border: 2px solid #e9ecef;
248
+ transition: transform 0.2s, box-shadow 0.2s;
249
+ }
250
+
251
+ .phase-card:hover {
252
+ transform: translateY(-2px);
253
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
254
+ }
255
+
256
+ .phase-card h2 {
257
+ color: #495057;
258
+ margin-bottom: 25px;
259
+ font-size: 1.8em;
260
+ border-bottom: 3px solid #667eea;
261
+ padding-bottom: 10px;
262
+ }
263
+
264
+ .metric-section {
265
+ margin-bottom: 30px;
266
+ }
267
+
268
+ .metric-section h3 {
269
+ color: #6c757d;
270
+ font-size: 1.1em;
271
+ margin-bottom: 15px;
272
+ text-transform: uppercase;
273
+ letter-spacing: 1px;
274
+ }
275
+
276
+ table {
277
+ width: 100%;
278
+ border-collapse: collapse;
279
+ background: white;
280
+ border-radius: 8px;
281
+ overflow: hidden;
282
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
283
+ }
284
+
285
+ th {
286
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
287
+ color: white;
288
+ padding: 12px 15px;
289
+ text-align: left;
290
+ font-weight: 600;
291
+ font-size: 0.9em;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.5px;
294
+ }
295
+
296
+ td {
297
+ padding: 12px 15px;
298
+ border-bottom: 1px solid #e9ecef;
299
+ color: #495057;
300
+ }
301
+
302
+ tr:last-child td {
303
+ border-bottom: none;
304
+ }
305
+
306
+ tr:hover {
307
+ background: #f8f9fa;
308
+ }
309
+
310
+ .value-highlight {
311
+ font-weight: 600;
312
+ color: #667eea;
313
+ }
314
+
315
+ .error-section {
316
+ background: #fff3cd;
317
+ border: 2px solid #ffc107;
318
+ border-radius: 8px;
319
+ padding: 20px;
320
+ margin-top: 20px;
321
+ }
322
+
323
+ .error-section h3 {
324
+ color: #856404;
325
+ margin-bottom: 10px;
326
+ }
327
+
328
+ .error-section p {
329
+ color: #856404;
330
+ margin: 5px 0;
331
+ }
332
+
333
+ .chart-container {
334
+ margin-top: 20px;
335
+ position: relative;
336
+ height: 300px;
337
+ background: white;
338
+ border-radius: 8px;
339
+ padding: 20px;
340
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
341
+ }
342
+
343
+ .summary {
344
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
345
+ color: white;
346
+ padding: 40px;
347
+ text-align: center;
348
+ }
349
+
350
+ .summary h2 {
351
+ margin-bottom: 20px;
352
+ font-size: 2em;
353
+ }
354
+
355
+ .summary-stats {
356
+ display: grid;
357
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
358
+ gap: 20px;
359
+ margin-top: 30px;
360
+ }
361
+
362
+ .summary-stat {
363
+ background: rgba(255, 255, 255, 0.1);
364
+ padding: 20px;
365
+ border-radius: 8px;
366
+ backdrop-filter: blur(10px);
367
+ }
368
+
369
+ .summary-stat label {
370
+ display: block;
371
+ font-size: 0.85em;
372
+ opacity: 0.9;
373
+ margin-bottom: 8px;
374
+ text-transform: uppercase;
375
+ letter-spacing: 1px;
376
+ }
377
+
378
+ .summary-stat value {
379
+ display: block;
380
+ font-size: 2em;
381
+ font-weight: 700;
382
+ }
383
+ </style>
384
+ </head>
385
+ <body>
386
+ <div class="container">
387
+ <div class="header">
388
+ <h1>šŸš€ Performance Test Report</h1>
389
+ <div class="timestamp">Generated: ${new Date(timestamp).toLocaleString()}</div>
390
+ </div>
391
+
392
+ <div class="config-section">
393
+ <h2>Test Configuration</h2>
394
+ <div class="config-grid">
395
+ <div class="config-item">
396
+ <label>API URL</label>
397
+ <value>${config.apiUrl}</value>
398
+ </div>
399
+ <div class="config-item">
400
+ <label>Create Count</label>
401
+ <value>${config.createCount.toLocaleString()}</value>
402
+ </div>
403
+ <div class="config-item">
404
+ <label>Get Count</label>
405
+ <value>${config.getCount.toLocaleString()}</value>
406
+ </div>
407
+ <div class="config-item">
408
+ <label>Connections</label>
409
+ <value>${config.connections}</value>
410
+ </div>
411
+ <div class="config-item">
412
+ <label>Duration</label>
413
+ <value>${config.duration}s</value>
414
+ </div>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="phases">
419
+ ${results
420
+ .map(
421
+ (result) => `
422
+ <div class="phase-card">
423
+ <h2>${result.phase}</h2>
424
+
425
+ <div class="metric-section">
426
+ <h3>šŸ“Š Requests</h3>
427
+ <table>
428
+ <thead>
429
+ <tr>
430
+ <th>Metric</th>
431
+ <th>Value</th>
432
+ </tr>
433
+ </thead>
434
+ <tbody>
435
+ <tr>
436
+ <td>Total</td>
437
+ <td class="value-highlight">${result.requests.total.toLocaleString()}</td>
438
+ </tr>
439
+ <tr>
440
+ <td>Average</td>
441
+ <td class="value-highlight">${formatNumber(result.requests.average)} req/s</td>
442
+ </tr>
443
+ <tr>
444
+ <td>Min</td>
445
+ <td>${formatNumber(result.requests.min)} req/s</td>
446
+ </tr>
447
+ <tr>
448
+ <td>Max</td>
449
+ <td>${formatNumber(result.requests.max)} req/s</td>
450
+ </tr>
451
+ </tbody>
452
+ </table>
453
+ </div>
454
+
455
+ <div class="metric-section">
456
+ <h3>ā±ļø Latency</h3>
457
+ <table>
458
+ <thead>
459
+ <tr>
460
+ <th>Percentile</th>
461
+ <th>Value (ms)</th>
462
+ </tr>
463
+ </thead>
464
+ <tbody>
465
+ <tr>
466
+ <td>Average</td>
467
+ <td class="value-highlight">${formatNumber(result.latency.average)}</td>
468
+ </tr>
469
+ <tr>
470
+ <td>Min</td>
471
+ <td>${formatNumber(result.latency.min)}</td>
472
+ </tr>
473
+ <tr>
474
+ <td>Max</td>
475
+ <td>${formatNumber(result.latency.max)}</td>
476
+ </tr>
477
+ <tr>
478
+ <td>p50</td>
479
+ <td>${formatNumber(result.latency.p50)}</td>
480
+ </tr>
481
+ <tr>
482
+ <td>p90</td>
483
+ <td>${formatNumber(result.latency.p90)}</td>
484
+ </tr>
485
+ <tr>
486
+ <td>p99</td>
487
+ <td>${formatNumber(result.latency.p99)}</td>
488
+ </tr>
489
+ <tr>
490
+ <td>p99.9</td>
491
+ <td>${formatNumber(result.latency.p99_9)}</td>
492
+ </tr>
493
+ </tbody>
494
+ </table>
495
+ </div>
496
+
497
+ <div class="metric-section">
498
+ <h3>šŸ“ˆ Throughput</h3>
499
+ <table>
500
+ <thead>
501
+ <tr>
502
+ <th>Metric</th>
503
+ <th>Value</th>
504
+ </tr>
505
+ </thead>
506
+ <tbody>
507
+ <tr>
508
+ <td>Average</td>
509
+ <td class="value-highlight">${formatBytes(result.throughput.average)}/s</td>
510
+ </tr>
511
+ <tr>
512
+ <td>Min</td>
513
+ <td>${formatBytes(result.throughput.min)}/s</td>
514
+ </tr>
515
+ <tr>
516
+ <td>Max</td>
517
+ <td>${formatBytes(result.throughput.max)}/s</td>
518
+ </tr>
519
+ </tbody>
520
+ </table>
521
+ </div>
522
+
523
+ <div class="metric-section">
524
+ <h3>šŸ“‰ Latency Distribution</h3>
525
+ <div class="chart-container">
526
+ <canvas id="latency-chart-${result.phase.replace(/\s+/g, '-').toLowerCase()}"></canvas>
527
+ </div>
528
+ </div>
529
+
530
+ ${
531
+ result.errors > 0 || result.timeouts > 0
532
+ ? `
533
+ <div class="error-section">
534
+ <h3>āš ļø Errors</h3>
535
+ ${result.errors > 0 ? `<p><strong>Errors:</strong> ${result.errors.toLocaleString()}</p>` : ''}
536
+ ${result.timeouts > 0 ? `<p><strong>Timeouts:</strong> ${result.timeouts.toLocaleString()}</p>` : ''}
537
+ </div>
538
+ `
539
+ : ''
540
+ }
541
+ </div>
542
+ `,
543
+ )
544
+ .join('')}
545
+ </div>
546
+
547
+ <div class="summary">
548
+ <h2>Summary</h2>
549
+ <div class="summary-stats">
550
+ ${
551
+ createResult
552
+ ? `
553
+ <div class="summary-stat">
554
+ <label>Create - Avg Latency</label>
555
+ <value>${formatNumber(createResult.latency.average)}ms</value>
556
+ </div>
557
+ <div class="summary-stat">
558
+ <label>Create - Requests/s</label>
559
+ <value>${formatNumber(createResult.requests.average)}</value>
560
+ </div>
561
+ `
562
+ : ''
563
+ }
564
+ ${
565
+ getResult
566
+ ? `
567
+ <div class="summary-stat">
568
+ <label>Get - Avg Latency</label>
569
+ <value>${formatNumber(getResult.latency.average)}ms</value>
570
+ </div>
571
+ <div class="summary-stat">
572
+ <label>Get - Requests/s</label>
573
+ <value>${formatNumber(getResult.requests.average)}</value>
574
+ </div>
575
+ `
576
+ : ''
577
+ }
578
+ </div>
579
+ </div>
580
+ </div>
581
+
582
+ <script>
583
+ const results = ${JSON.stringify(results)};
584
+
585
+ results.forEach((result, index) => {
586
+ const canvasId = 'latency-chart-' + result.phase.replace(/\\s+/g, '-').toLowerCase();
587
+ const ctx = document.getElementById(canvasId);
588
+ if (!ctx) return;
589
+
590
+ new Chart(ctx, {
591
+ type: 'bar',
592
+ data: {
593
+ labels: ['p50', 'p90', 'p99', 'p99.9'],
594
+ datasets: [{
595
+ label: 'Latency (ms)',
596
+ data: [
597
+ result.latency.p50,
598
+ result.latency.p90,
599
+ result.latency.p99,
600
+ result.latency.p99_9
601
+ ],
602
+ backgroundColor: [
603
+ 'rgba(102, 126, 234, 0.8)',
604
+ 'rgba(118, 75, 162, 0.8)',
605
+ 'rgba(255, 99, 132, 0.8)',
606
+ 'rgba(255, 159, 64, 0.8)'
607
+ ],
608
+ borderColor: [
609
+ 'rgba(102, 126, 234, 1)',
610
+ 'rgba(118, 75, 162, 1)',
611
+ 'rgba(255, 99, 132, 1)',
612
+ 'rgba(255, 159, 64, 1)'
613
+ ],
614
+ borderWidth: 2
615
+ }]
616
+ },
617
+ options: {
618
+ responsive: true,
619
+ maintainAspectRatio: false,
620
+ plugins: {
621
+ legend: {
622
+ display: false
623
+ },
624
+ title: {
625
+ display: true,
626
+ text: 'Latency Percentiles',
627
+ font: {
628
+ size: 16,
629
+ weight: 'bold'
630
+ }
631
+ }
632
+ },
633
+ scales: {
634
+ y: {
635
+ beginAtZero: true,
636
+ title: {
637
+ display: true,
638
+ text: 'Latency (ms)'
639
+ }
640
+ }
641
+ }
642
+ }
643
+ });
644
+ });
645
+ </script>
646
+ </body>
647
+ </html>`;
648
+ }
649
+
650
+ function saveHTMLReport(html: string, outputPath: string | null): string {
651
+ const defaultPath = path.join(process.cwd(), 'perf', `performance-report-${Date.now()}.html`);
652
+ const finalPath = outputPath || defaultPath;
653
+
654
+ // Ensure directory exists
655
+ const dir = path.dirname(finalPath);
656
+ if (!fs.existsSync(dir)) {
657
+ fs.mkdirSync(dir, { recursive: true });
658
+ }
659
+
660
+ fs.writeFileSync(finalPath, html, 'utf-8');
661
+ return finalPath;
662
+ }
663
+
664
+ async function runCreatePhase(
665
+ config: PerformanceConfig,
666
+ itemIds: string[],
667
+ userIds: string[],
668
+ ): Promise<TestResult> {
669
+ const baseUrl = new URL(config.apiUrl);
670
+ const paths: string[] = [];
671
+
672
+ // Generate unique paths for creating items
673
+ for (let i = 0; i < config.createCount; i++) {
674
+ const userId = userIds[i % userIds.length];
675
+ const itemId = itemIds[i];
676
+ paths.push(`/v1/user/${userId}/dismissible-item/${itemId}`);
677
+ }
678
+
679
+ const instance = autocannon(
680
+ {
681
+ url: baseUrl.origin,
682
+ connections: config.connections,
683
+ duration: config.duration,
684
+ setupClient: createPathCycler(paths) as autocannon.SetupClientFunction,
685
+ },
686
+ (err, _result) => {
687
+ if (err) {
688
+ console.error('Error during create phase:', err);
689
+ throw err;
690
+ }
691
+ },
692
+ );
693
+
694
+ return new Promise((resolve, reject) => {
695
+ instance.on('done', (result) => {
696
+ const testResult: TestResult = {
697
+ phase: 'Create Items',
698
+ requests: {
699
+ total: result.requests.total,
700
+ average: result.requests.average,
701
+ min: result.requests.min,
702
+ max: result.requests.max,
703
+ },
704
+ latency: {
705
+ average: result.latency.average,
706
+ min: result.latency.min,
707
+ max: result.latency.max,
708
+ p50: result.latency.p50,
709
+ p90: result.latency.p90,
710
+ p99: result.latency.p99,
711
+ p99_9: result.latency.p99_9,
712
+ },
713
+ throughput: {
714
+ average: result.throughput.average,
715
+ min: result.throughput.min,
716
+ max: result.throughput.max,
717
+ },
718
+ errors: result.errors,
719
+ timeouts: result.timeouts,
720
+ duration: result.duration,
721
+ };
722
+ resolve(testResult);
723
+ });
724
+
725
+ instance.on('error', (err) => {
726
+ reject(err);
727
+ });
728
+ });
729
+ }
730
+
731
+ async function runGetPhase(
732
+ config: PerformanceConfig,
733
+ itemIds: string[],
734
+ userIds: string[],
735
+ ): Promise<TestResult> {
736
+ const baseUrl = new URL(config.apiUrl);
737
+ const paths: string[] = [];
738
+
739
+ // Use existing items from create phase (limited to getCount)
740
+ const itemsToGet = Math.min(config.getCount, itemIds.length);
741
+ for (let i = 0; i < itemsToGet; i++) {
742
+ const userId = userIds[i % userIds.length];
743
+ const itemId = itemIds[i];
744
+ paths.push(`/v1/user/${userId}/dismissible-item/${itemId}`);
745
+ }
746
+
747
+ const instance = autocannon(
748
+ {
749
+ url: baseUrl.origin,
750
+ connections: config.connections,
751
+ duration: config.duration,
752
+ setupClient: createPathCycler(paths) as autocannon.SetupClientFunction,
753
+ },
754
+ (err, _result) => {
755
+ if (err) {
756
+ console.error('Error during get phase:', err);
757
+ throw err;
758
+ }
759
+ },
760
+ );
761
+
762
+ return new Promise((resolve, reject) => {
763
+ instance.on('done', (result) => {
764
+ const testResult: TestResult = {
765
+ phase: 'Get Items',
766
+ requests: {
767
+ total: result.requests.total,
768
+ average: result.requests.average,
769
+ min: result.requests.min,
770
+ max: result.requests.max,
771
+ },
772
+ latency: {
773
+ average: result.latency.average,
774
+ min: result.latency.min,
775
+ max: result.latency.max,
776
+ p50: result.latency.p50,
777
+ p90: result.latency.p90,
778
+ p99: result.latency.p99,
779
+ p99_9: result.latency.p99_9,
780
+ },
781
+ throughput: {
782
+ average: result.throughput.average,
783
+ min: result.throughput.min,
784
+ max: result.throughput.max,
785
+ },
786
+ errors: result.errors,
787
+ timeouts: result.timeouts,
788
+ duration: result.duration,
789
+ };
790
+ resolve(testResult);
791
+ });
792
+
793
+ instance.on('error', (err) => {
794
+ reject(err);
795
+ });
796
+ });
797
+ }
798
+
799
+ async function main(): Promise<void> {
800
+ const config = getConfig();
801
+
802
+ console.log('šŸš€ Performance Test Configuration:');
803
+ console.log(` API URL: ${config.apiUrl}`);
804
+ console.log(` Create Count: ${config.createCount.toLocaleString()}`);
805
+ console.log(` Get Count: ${config.getCount.toLocaleString()}`);
806
+ console.log(` Connections: ${config.connections}`);
807
+ console.log(` Duration: ${config.duration}s`);
808
+ console.log('');
809
+
810
+ // Generate unique itemIds and userIds
811
+ const itemIds: string[] = [];
812
+ const userIds: string[] = [];
813
+
814
+ for (let i = 0; i < Math.max(config.createCount, config.getCount); i++) {
815
+ itemIds.push(`perf-test-item-${i + 1}`);
816
+ }
817
+
818
+ // Use a smaller set of userIds to simulate multiple items per user
819
+ const numUsers = Math.max(10, Math.floor(config.createCount / 10));
820
+ for (let i = 0; i < numUsers; i++) {
821
+ userIds.push(`perf-test-user-${i + 1}`);
822
+ }
823
+
824
+ try {
825
+ // Phase 1: Create items
826
+ console.log('šŸ“ Phase 1: Creating items...');
827
+ const createResult = await runCreatePhase(config, itemIds, userIds);
828
+ printResults(createResult);
829
+
830
+ // Small delay to ensure items are persisted
831
+ await new Promise((resolve) => setTimeout(resolve, 1000));
832
+
833
+ // Phase 2: Get existing items
834
+ console.log('šŸ“– Phase 2: Getting existing items...');
835
+ const getResult = await runGetPhase(config, itemIds, userIds);
836
+ printResults(getResult);
837
+
838
+ // Generate and save HTML report
839
+ const outputPath = getOutputPath();
840
+ const htmlReport = generateHTMLReport([createResult, getResult], config);
841
+ const savedPath = saveHTMLReport(htmlReport, outputPath);
842
+
843
+ console.log(`\nšŸ“„ HTML report saved to: ${savedPath}`);
844
+ console.log('āœ… Performance test completed successfully!');
845
+ } catch (error) {
846
+ console.error('āŒ Performance test failed:', error);
847
+ process.exit(1);
848
+ }
849
+ }
850
+
851
+ // Run the test
852
+ main().catch((error) => {
853
+ console.error('Fatal error:', error);
854
+ process.exit(1);
855
+ });