@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.
- package/config/.env.yaml +17 -1
- package/package.json +13 -10
- package/scripts/performance-test.config.json +29 -0
- package/scripts/performance-test.ts +855 -0
- package/src/app-test.factory.ts +8 -1
- package/src/app.e2e-spec.ts +1 -1
- package/src/app.setup.ts +36 -0
- package/src/bootstrap.ts +8 -3
- package/src/config/default-app.config.ts +10 -0
- package/src/cors/cors.config.spec.ts +162 -0
- package/src/cors/cors.config.ts +37 -0
- package/src/cors/index.ts +1 -0
- package/src/health/health.controller.spec.ts +7 -31
- package/src/health/health.controller.ts +4 -5
- package/src/health/health.module.ts +2 -3
- package/src/health/index.ts +0 -1
- package/src/helmet/helmet.config.spec.ts +197 -0
- package/src/helmet/helmet.config.ts +60 -0
- package/src/helmet/index.ts +1 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/transform-boolean.decorator.spec.ts +47 -0
- package/src/utils/transform-boolean.decorator.ts +19 -0
- package/src/utils/transform-comma-separated.decorator.ts +16 -0
- package/src/health/health.service.spec.ts +0 -46
- package/src/health/health.service.ts +0 -16
|
@@ -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
|
+
});
|