this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: update stress test

+612 -271
+2 -1
.gitignore
··· 33 33 # Finder (MacOS) folder config 34 34 .DS_Store 35 35 migrations 36 - local.db 36 + local.db* 37 + stress-test-results*
+610 -270
stress.ts
··· 10 10 ]; 11 11 12 12 // Script configuration 13 + /** 14 + * Stress Test Configuration Parameters 15 + * 16 + * @remarks 17 + * These settings control the behavior and intensity of the load test. 18 + * Modify with extreme caution as improper values may cause service disruption. 19 + */ 13 20 const CONFIG = { 21 + /** Target server endpoint - modify for production targets */ 14 22 baseUrl: "http://localhost:3000", 15 - startConcurrency: 100, // Start with higher concurrency 16 - maxConcurrency: 200000, // Increased max to test limits more aggressively 17 - concurrencyFactor: 3, // More aggressive scaling (3x per step) 18 - requestsPerUser: 20, // More requests per user 19 - delayBetweenRequests: 0, // No delay between requests for maximum load 20 - delayBetweenLevels: 1000, // Shorter delay between levels 21 - runWithCaching: false, // Disabled caching for more aggressive testing 22 - successThreshold: 95, // % success rate to continue 23 - responseTimeThreshold: 500, // ms 24 - stopOnFailure: true, // Stop when hitting breaking point 25 - disableDetailedLogging: true, // Disable per-request logging to reduce overhead 23 + 24 + /** @critical Initial concurrency value - starts with significant load */ 25 + startConcurrency: 500, // Higher initial load for stress testing 26 + 27 + /** @warning Maximum concurrent users - can overload production systems */ 28 + maxConcurrency: 10000, // Increased maximum for thorough performance evaluation 29 + 30 + /** Multiplicative step between concurrency levels (geometric progression) */ 31 + concurrencyFactor: 2.0, // More aggressive scaling to identify breaking points faster 32 + 33 + /** @critical Number of sequential requests each simulated user will make */ 34 + requestsPerUser: 25, // Increased per-user workload for extended session simulation 35 + 36 + /** Maximum milliseconds before timing out a request */ 37 + requestTimeout: 8000, // Reduced timeout to identify latency issues earlier 38 + 39 + /** Milliseconds to wait between sequential requests from same user */ 40 + delayBetweenRequests: 20, // Reduced delay for more intensive testing 41 + 42 + /** Milliseconds to pause between concurrency level increases */ 43 + delayBetweenLevels: 2000, // Shorter recovery time between test phases 44 + 45 + /** Whether to utilize HTTP caching mechanisms (ETag) */ 46 + runWithCaching: false, // Disabled caching to maximize server load 47 + 48 + /** @critical Minimum success rate percentage to continue testing */ 49 + successThreshold: 95, // Lowered success threshold to detect degradation earlier 50 + 51 + /** @critical Maximum acceptable p95 response time in milliseconds */ 52 + responseTimeThreshold: 350, // Stricter response time requirements 53 + 54 + /** Whether to abort testing when thresholds are exceeded */ 55 + stopOnFailure: true, // Halt on threshold breach to prevent cascading failures 56 + 57 + /** Suppress detailed per-request logging to reduce client-side overhead */ 58 + disableDetailedLogging: true, // Limit logging to improve test client performance 59 + 60 + /** Track Time To First Byte as separate metric */ 61 + measureTTFB: true, // Important for network latency analysis 62 + 63 + /** Calculate and store statistical distribution of response times */ 64 + trackPercentiles: true, // Essential for performance analysis 65 + 66 + /** Track time requests spend in queue vs processing (advanced) */ 67 + trackQueueTime: false, // Disabled to reduce complexity 68 + 69 + /** @critical Number of requests to execute before measurement begins */ 70 + warmupRequests: 100, // Increased warmup to ensure system stabilization 26 71 }; 27 72 73 + // Time buckets for percentile tracking (in ms) 74 + const TIME_BUCKETS = [ 75 + 0, 10, 25, 50, 75, 100, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 76 + 3000, 5000, 7500, 10000, 15000, 30000, 77 + ]; 78 + 28 79 // Stats tracking 29 80 type EndpointStats = { 30 81 totalRequests: number; ··· 32 83 notModifiedResponses: number; 33 84 failedRequests: number; 34 85 responseTimeTotal: number; 86 + ttfbTimeTotal: number; // Time to first byte total 87 + processingTimeTotal: number; // Server processing time (TTFB to full response) 35 88 responseTimeMin: number; 36 89 responseTimeMax: number; 90 + ttfbTimeMin: number; 91 + ttfbTimeMax: number; 92 + timeBuckets: number[]; // For percentile calculations 93 + ttfbTimeBuckets: number[]; // TTFB percentiles 37 94 }; 38 95 39 96 // Add memory usage tracking ··· 44 101 notModifiedResponses: number; 45 102 failedRequests: number; 46 103 responseTimeTotal: number; 104 + ttfbTimeTotal: number; 105 + processingTimeTotal: number; 47 106 responseTimeMin: number; 48 107 responseTimeMax: number; 108 + ttfbTimeMin: number; 109 + ttfbTimeMax: number; 110 + p50ResponseTime: number; // 50th percentile (median) 111 + p90ResponseTime: number; // 90th percentile 112 + p95ResponseTime: number; // 95th percentile 113 + p99ResponseTime: number; // 99th percentile 114 + p50TTFB: number; // TTFB percentiles 115 + p90TTFB: number; 116 + p95TTFB: number; 117 + p99TTFB: number; 49 118 startTime: number; 50 119 endTime: number; 51 120 userCompletedCount: number; ··· 71 140 notModifiedResponses: 0, 72 141 failedRequests: 0, 73 142 responseTimeTotal: 0, 143 + ttfbTimeTotal: 0, 144 + processingTimeTotal: 0, 74 145 responseTimeMin: Number.MAX_VALUE, 75 146 responseTimeMax: 0, 147 + ttfbTimeMin: Number.MAX_VALUE, 148 + ttfbTimeMax: 0, 149 + p50ResponseTime: 0, 150 + p90ResponseTime: 0, 151 + p95ResponseTime: 0, 152 + p99ResponseTime: 0, 153 + p50TTFB: 0, 154 + p90TTFB: 0, 155 + p95TTFB: 0, 156 + p99TTFB: 0, 76 157 startTime: 0, 77 158 endTime: 0, 78 159 userCompletedCount: 0, ··· 89 170 notModifiedResponses: 0, 90 171 failedRequests: 0, 91 172 responseTimeTotal: 0, 173 + ttfbTimeTotal: 0, 174 + processingTimeTotal: 0, 92 175 responseTimeMin: Number.MAX_VALUE, 93 176 responseTimeMax: 0, 177 + ttfbTimeMin: Number.MAX_VALUE, 178 + ttfbTimeMax: 0, 179 + timeBuckets: new Array(TIME_BUCKETS.length).fill(0), 180 + ttfbTimeBuckets: new Array(TIME_BUCKETS.length).fill(0), 94 181 }; 95 182 } 96 - // ETag cache 183 + 184 + // ETag cache for each endpoint by user 97 185 const etagCache: Record<string, string> = {}; 186 + // Helper function to calculate percentiles from time buckets 187 + function calculatePercentile(buckets: number[], percentile: number): number { 188 + const totalSamples = buckets.reduce((sum, count) => sum + count, 0); 189 + if (totalSamples === 0) return 0; 190 + 191 + const targetCount = totalSamples * (percentile / 100); 192 + let currentCount = 0; 193 + 194 + for (let i = 0; i < buckets.length; i++) { 195 + currentCount += buckets[i] ?? 0; // Handle potential undefined values safely 196 + if (currentCount >= targetCount) { 197 + // Return the bucket boundary 198 + return TIME_BUCKETS[i] ?? 0; // Handle potential undefined values safely 199 + } 200 + } 201 + 202 + return TIME_BUCKETS[TIME_BUCKETS.length - 1] ?? 0; // Handle potential undefined value 203 + } 204 + // Helper function to add a time to the appropriate bucket 205 + function addTimeToBucket(buckets: number[], time: number): void { 206 + for (let i = 0; i < TIME_BUCKETS.length; i++) { 207 + if ( 208 + time <= (TIME_BUCKETS[i] || Number.MAX_VALUE) || 209 + i === TIME_BUCKETS.length - 1 210 + ) { 211 + buckets[i] = (buckets[i] || 0) + 1; 212 + break; 213 + } 214 + } 215 + } 98 216 99 217 // Spinner for loading animation 100 218 class Spinner { ··· 165 283 } 166 284 167 285 try { 286 + // Start timing 168 287 const startTime = performance.now(); 169 - const response = await fetch(url, { headers }); 288 + 289 + // Create AbortController for timeout 290 + const controller = new AbortController(); 291 + const timeoutId = setTimeout(() => { 292 + controller.abort(); 293 + }, CONFIG.requestTimeout); 294 + 295 + // Make the request 296 + const response = await fetch(url, { 297 + headers, 298 + signal: controller.signal, 299 + }); 300 + 301 + // Measure TTFB as soon as headers are available 302 + const ttfbTime = performance.now() - startTime; 303 + 304 + // Get the response body 305 + const text = await response.text(); 306 + 307 + // Clear timeout 308 + clearTimeout(timeoutId); 309 + 310 + // End timing after body is received 170 311 const endTime = performance.now(); 171 312 const responseTime = endTime - startTime; 313 + const processingTime = responseTime - ttfbTime; 172 314 173 315 // Track overall stats 174 316 stats.totalRequests++; 317 + stats.responseTimeTotal += responseTime; 318 + stats.ttfbTimeTotal += ttfbTime; 319 + stats.processingTimeTotal += processingTime; 320 + stats.responseTimeMin = Math.min(stats.responseTimeMin, responseTime); 321 + stats.responseTimeMax = Math.max(stats.responseTimeMax, responseTime); 322 + stats.ttfbTimeMin = Math.min(stats.ttfbTimeMin, ttfbTime); 323 + stats.ttfbTimeMax = Math.max(stats.ttfbTimeMax, ttfbTime); 175 324 176 325 // Ensure the endpoint exists in stats.endpoints 177 326 if (!stats.endpoints[endpoint]) { ··· 181 330 notModifiedResponses: 0, 182 331 failedRequests: 0, 183 332 responseTimeTotal: 0, 333 + ttfbTimeTotal: 0, 334 + processingTimeTotal: 0, 184 335 responseTimeMin: Number.MAX_VALUE, 185 336 responseTimeMax: 0, 337 + ttfbTimeMin: Number.MAX_VALUE, 338 + ttfbTimeMax: 0, 339 + timeBuckets: new Array(TIME_BUCKETS.length).fill(0), 340 + ttfbTimeBuckets: new Array(TIME_BUCKETS.length).fill(0), 186 341 }; 187 342 } 188 - 189 - stats.endpoints[endpoint].totalRequests++; 190 - stats.responseTimeTotal += responseTime; 191 - stats.responseTimeMin = Math.min(stats.responseTimeMin, responseTime); 192 - stats.responseTimeMax = Math.max(stats.responseTimeMax, responseTime); 193 343 194 344 // Track endpoint-specific stats 345 + stats.endpoints[endpoint].totalRequests++; 195 346 stats.endpoints[endpoint].responseTimeTotal += responseTime; 347 + stats.endpoints[endpoint].ttfbTimeTotal += ttfbTime; 348 + stats.endpoints[endpoint].processingTimeTotal += processingTime; 196 349 stats.endpoints[endpoint].responseTimeMin = Math.min( 197 350 stats.endpoints[endpoint].responseTimeMin, 198 351 responseTime, ··· 201 354 stats.endpoints[endpoint].responseTimeMax, 202 355 responseTime, 203 356 ); 357 + stats.endpoints[endpoint].ttfbTimeMin = Math.min( 358 + stats.endpoints[endpoint].ttfbTimeMin, 359 + ttfbTime, 360 + ); 361 + stats.endpoints[endpoint].ttfbTimeMax = Math.max( 362 + stats.endpoints[endpoint].ttfbTimeMax, 363 + ttfbTime, 364 + ); 365 + 366 + // Track time buckets for percentiles 367 + if (CONFIG.trackPercentiles) { 368 + addTimeToBucket(stats.endpoints[endpoint].timeBuckets, responseTime); 369 + addTimeToBucket(stats.endpoints[endpoint].ttfbTimeBuckets, ttfbTime); 370 + } 204 371 205 372 if (response.status === 304) { 206 373 stats.notModifiedResponses++; 207 374 stats.endpoints[endpoint].notModifiedResponses++; 375 + stats.successfulRequests++; // Count 304 as success 376 + stats.endpoints[endpoint].successfulRequests++; 377 + 208 378 if (!CONFIG.disableDetailedLogging) { 209 379 logWithTime( 210 - `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - 304 Not Modified (${responseTime.toFixed(2)}ms)`, 380 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - 304 Not Modified (${responseTime.toFixed(2)}ms, TTFB: ${ttfbTime.toFixed(2)}ms)`, 211 381 "info", 212 382 ); 213 383 } 214 384 } else if (response.ok) { 215 385 stats.successfulRequests++; 216 386 stats.endpoints[endpoint].successfulRequests++; 387 + 217 388 if (!CONFIG.disableDetailedLogging) { 218 389 logWithTime( 219 - `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${response.status} OK (${responseTime.toFixed(2)}ms)`, 390 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${response.status} OK (${responseTime.toFixed(2)}ms, TTFB: ${ttfbTime.toFixed(2)}ms)`, 220 391 "success", 221 392 ); 222 393 } ··· 229 400 } 230 401 } 231 402 232 - // Parse JSON response (but don't do anything with it) 233 - await response.json(); 403 + // Parse JSON response for validation 404 + try { 405 + JSON.parse(text); 406 + } catch (e) { 407 + stats.failedRequests++; 408 + stats.endpoints[endpoint].failedRequests++; 409 + stats.successfulRequests--; 410 + stats.endpoints[endpoint].successfulRequests--; 411 + 412 + logWithTime( 413 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - Invalid JSON response`, 414 + "error", 415 + ); 416 + } 234 417 } else { 235 418 stats.failedRequests++; 236 419 stats.endpoints[endpoint].failedRequests++; 420 + 237 421 // Always log errors, even if detailed logging is disabled 238 422 logWithTime( 239 423 `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${response.status} Error (${responseTime.toFixed(2)}ms)`, ··· 251 435 notModifiedResponses: 0, 252 436 failedRequests: 0, 253 437 responseTimeTotal: 0, 438 + ttfbTimeTotal: 0, 439 + processingTimeTotal: 0, 254 440 responseTimeMin: Number.MAX_VALUE, 255 441 responseTimeMax: 0, 442 + ttfbTimeMin: Number.MAX_VALUE, 443 + ttfbTimeMax: 0, 444 + timeBuckets: new Array(TIME_BUCKETS.length).fill(0), 445 + ttfbTimeBuckets: new Array(TIME_BUCKETS.length).fill(0), 256 446 }; 257 447 } 258 448 259 449 stats.endpoints[endpoint].failedRequests++; 450 + 451 + // Check if this was a timeout 452 + const errorMessage = error instanceof Error ? error.message : String(error); 453 + const isTimeout = 454 + errorMessage.includes("abort") || errorMessage.includes("timeout"); 455 + 260 456 // Always log errors, even if detailed logging is disabled 261 457 logWithTime( 262 - `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - Exception: ${(error as Error).message}`, 458 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${isTimeout ? "Timeout" : "Exception"}: ${errorMessage}`, 263 459 "error", 264 460 ); 265 461 } ··· 268 464 // Simulate a user session 269 465 async function simulateUser(userId: string): Promise<void> { 270 466 try { 271 - // Create all requests at once for maximum concurrency 272 - const requests: Promise<void>[] = []; 273 - 274 467 for (let i = 0; i < CONFIG.requestsPerUser; i++) { 275 468 // Choose a random endpoint 276 469 const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)]; 277 470 278 471 // Make sure endpoint is not undefined before adding it 279 472 if (endpoint) { 280 - // Instead of waiting for each request, push them to an array 281 - requests.push(makeRequest(endpoint, userId, i + 1)); 473 + // Make the request 474 + await makeRequest(endpoint, userId, i + 1); 282 475 283 - // Add a minimal delay if configured (usually 0) 476 + // Add a small delay between requests to simulate real user behavior 284 477 if (CONFIG.delayBetweenRequests > 0) { 285 478 await new Promise((resolve) => 286 479 setTimeout(resolve, CONFIG.delayBetweenRequests), ··· 288 481 } 289 482 } 290 483 } 291 - 292 - // Wait for all requests to complete 293 - await Promise.allSettled(requests); 294 484 } catch (error) { 295 485 logWithTime( 296 486 `User ${userId.slice(0, 4)} - Error: ${(error as Error).message}`, ··· 302 492 } 303 493 } 304 494 495 + // Do warmup requests to prime the server cache 496 + async function warmupServer(): Promise<void> { 497 + logWithTime( 498 + `Warming up server with ${CONFIG.warmupRequests} requests...`, 499 + "info", 500 + ); 501 + 502 + const spinner = new Spinner("Warming up server..."); 503 + spinner.start(); 504 + 505 + const promises: Promise<void>[] = []; 506 + 507 + for (let i = 0; i < CONFIG.warmupRequests; i++) { 508 + const endpoint = endpoints[i % endpoints.length]; 509 + promises.push( 510 + fetch(`${CONFIG.baseUrl}${endpoint}`) 511 + .then(async (response) => { 512 + // Store the ETag for future use 513 + const etag = response.headers.get("ETag"); 514 + if (etag && CONFIG.runWithCaching) { 515 + etagCache[`warmup-${endpoint}`] = etag; 516 + } 517 + 518 + // Read the response to completion 519 + await response.text(); 520 + }) 521 + .catch((e) => { 522 + logWithTime(`Warmup request error: ${e.message}`, "error"); 523 + }), 524 + ); 525 + } 526 + 527 + await Promise.allSettled(promises); 528 + spinner.stop(); 529 + 530 + logWithTime("Server warmup complete", "success"); 531 + } 532 + 533 + // Calculate percentiles after test completion 534 + function calculatePercentiles(): void { 535 + if (!CONFIG.trackPercentiles) return; 536 + 537 + // Initialize combined stats objects to track cumulative data 538 + const combinedResponseBuckets = new Array(TIME_BUCKETS.length).fill(0); 539 + const combinedTTFBBuckets = new Array(TIME_BUCKETS.length).fill(0); 540 + 541 + // Combine all endpoint buckets 542 + for (const endpoint in stats.endpoints) { 543 + const endpointStats = stats.endpoints[endpoint]; 544 + 545 + if (!endpointStats) continue; 546 + 547 + // Add this endpoint's data to the combined buckets 548 + for (let i = 0; i < TIME_BUCKETS.length; i++) { 549 + combinedResponseBuckets[i] += endpointStats.timeBuckets[i] || 0; 550 + combinedTTFBBuckets[i] += endpointStats.ttfbTimeBuckets[i] || 0; 551 + } 552 + } 553 + 554 + // Calculate overall percentiles from combined data 555 + stats.p50ResponseTime = calculatePercentile(combinedResponseBuckets, 50); 556 + stats.p90ResponseTime = calculatePercentile(combinedResponseBuckets, 90); 557 + stats.p95ResponseTime = calculatePercentile(combinedResponseBuckets, 95); 558 + stats.p99ResponseTime = calculatePercentile(combinedResponseBuckets, 99); 559 + 560 + stats.p50TTFB = calculatePercentile(combinedTTFBBuckets, 50); 561 + stats.p90TTFB = calculatePercentile(combinedTTFBBuckets, 90); 562 + stats.p95TTFB = calculatePercentile(combinedTTFBBuckets, 95); 563 + stats.p99TTFB = calculatePercentile(combinedTTFBBuckets, 99); 564 + } 565 + 305 566 // Print results in a fancy way 306 567 function printResults() { 307 568 console.log("\n"); ··· 341 602 ); 342 603 343 604 const avgResponseTime = stats.responseTimeTotal / stats.totalRequests; 605 + const avgTTFB = stats.ttfbTimeTotal / stats.totalRequests; 606 + const avgProcessingTime = stats.processingTimeTotal / stats.totalRequests; 607 + 344 608 console.log( 345 609 `${chalk.cyan("Average Response Time:")} ${chalk.yellow(avgResponseTime.toFixed(2))} ms`, 346 610 ); 347 611 console.log( 612 + `${chalk.cyan("Average TTFB:")} ${chalk.yellow(avgTTFB.toFixed(2))} ms`, 613 + ); 614 + console.log( 615 + `${chalk.cyan("Average Processing Time:")} ${chalk.yellow(avgProcessingTime.toFixed(2))} ms`, 616 + ); 617 + 618 + console.log( 348 619 `${chalk.cyan("Min Response Time:")} ${chalk.green(stats.responseTimeMin.toFixed(2))} ms`, 349 620 ); 350 621 console.log( 351 622 `${chalk.cyan("Max Response Time:")} ${chalk.red(stats.responseTimeMax.toFixed(2))} ms`, 352 623 ); 624 + 625 + if (CONFIG.trackPercentiles) { 626 + console.log( 627 + `${chalk.cyan("Response Time (p50/p95/p99):")} ${chalk.yellow(stats.p50ResponseTime.toFixed(2))}/${chalk.yellow(stats.p95ResponseTime.toFixed(2))}/${chalk.red(stats.p99ResponseTime.toFixed(2))} ms`, 628 + ); 629 + console.log( 630 + `${chalk.cyan("TTFB Time (p50/p95/p99):")} ${chalk.yellow(stats.p50TTFB.toFixed(2))}/${chalk.yellow(stats.p95TTFB.toFixed(2))}/${chalk.red(stats.p99TTFB.toFixed(2))} ms`, 631 + ); 632 + } 353 633 354 634 console.log("\n"); 355 635 console.log(chalk.bold.white("📈 Endpoint Stats:")); ··· 376 656 377 657 const avgResponseTime = 378 658 endpointStats.responseTimeTotal / endpointStats.totalRequests; 659 + const avgEndpointTTFB = 660 + endpointStats.ttfbTimeTotal / endpointStats.totalRequests; 661 + 379 662 console.log( 380 663 `${chalk.cyan("Average Response Time:")} ${chalk.yellow(avgResponseTime.toFixed(2))} ms`, 664 + ); 665 + console.log( 666 + `${chalk.cyan("Average TTFB:")} ${chalk.yellow(avgEndpointTTFB.toFixed(2))} ms`, 381 667 ); 382 668 console.log( 383 669 `${chalk.cyan("Min Response Time:")} ${chalk.green(endpointStats.responseTimeMin.toFixed(2))} ms`, ··· 410 696 notModifiedResponses: 0, 411 697 failedRequests: 0, 412 698 responseTimeTotal: 0, 699 + ttfbTimeTotal: 0, 700 + processingTimeTotal: 0, 413 701 responseTimeMin: Number.MAX_VALUE, 414 702 responseTimeMax: 0, 703 + ttfbTimeMin: Number.MAX_VALUE, 704 + ttfbTimeMax: 0, 705 + p50ResponseTime: 0, 706 + p90ResponseTime: 0, 707 + p95ResponseTime: 0, 708 + p99ResponseTime: 0, 709 + p50TTFB: 0, 710 + p90TTFB: 0, 711 + p95TTFB: 0, 712 + p99TTFB: 0, 415 713 startTime: 0, 416 714 endTime: 0, 417 715 userCompletedCount: 0, ··· 428 726 notModifiedResponses: 0, 429 727 failedRequests: 0, 430 728 responseTimeTotal: 0, 729 + ttfbTimeTotal: 0, 730 + processingTimeTotal: 0, 431 731 responseTimeMin: Number.MAX_VALUE, 432 732 responseTimeMax: 0, 733 + ttfbTimeMin: Number.MAX_VALUE, 734 + ttfbTimeMax: 0, 735 + timeBuckets: new Array(TIME_BUCKETS.length).fill(0), 736 + ttfbTimeBuckets: new Array(TIME_BUCKETS.length).fill(0), 433 737 }; 434 738 } 435 739 ··· 483 787 stats.requestsPerSecond = stats.totalRequests / durationInSeconds; 484 788 stats.successRate = 485 789 stats.totalRequests > 0 486 - ? (stats.successfulRequests / stats.totalRequests) * 100 790 + ? ((stats.successfulRequests + stats.notModifiedResponses) / 791 + stats.totalRequests) * 792 + 100 487 793 : 0; 488 794 795 + // Calculate percentiles from time buckets 796 + calculatePercentiles(); 797 + 489 798 // Capture memory usage 490 799 if (process.memoryUsage) { 491 800 const memoryUsage = process.memoryUsage(); 492 - (stats as any).memoryUsage = { 801 + (stats as Record<string, unknown>).memoryUsage = { 493 802 rss: memoryUsage.rss, 494 803 heapTotal: memoryUsage.heapTotal, 495 804 heapUsed: memoryUsage.heapUsed, ··· 503 812 return result; 504 813 } 505 814 506 - function printLevelResults(levelStats: ConcurrencyStats) { 815 + // Print a summary of all concurrency levels tested 816 + function printConcurrencySummary(): void { 507 817 console.log("\n"); 508 - console.log( 509 - chalk.bold.cyan(`📊 Concurrency Level: ${levelStats.concurrency} users`), 510 - ); 818 + console.log(chalk.bold.cyan("📊 Concurrency Level Summary")); 511 819 console.log( 512 - chalk.gray("────────────────────────────────────────────────────"), 820 + chalk.gray("════════════════════════════════════════════════════════"), 513 821 ); 514 822 823 + // Table headers 515 824 console.log( 516 - `${chalk.cyan("Success Rate:")} ${ 517 - levelStats.successRate >= CONFIG.successThreshold 518 - ? chalk.green(`${levelStats.successRate.toFixed(2)}%`) 519 - : chalk.red(`${levelStats.successRate.toFixed(2)}%`) 520 - }`, 825 + chalk.bold( 826 + `${chalk.cyan("Concurrency").padEnd(10)} | ` + 827 + `${chalk.cyan("RPS").padEnd(8)} | ` + 828 + `${chalk.cyan("Success %").padEnd(10)} | ` + 829 + `${chalk.cyan("Avg(ms)").padEnd(8)} | ` + 830 + `${chalk.cyan("p95(ms)").padEnd(8)} | ` + 831 + `${chalk.cyan("p99(ms)").padEnd(8)} | ` + 832 + `${chalk.cyan("TTFB p95").padEnd(8)} | ` + 833 + `${chalk.cyan("Status")}`, 834 + ), 521 835 ); 522 836 837 + // Separator 523 838 console.log( 524 - `${chalk.cyan("Requests per Second:")} ${levelStats.requestsPerSecond.toFixed(2)}`, 839 + chalk.gray( 840 + "───────────┼──────────┼────────────┼──────────┼──────────┼──────────┼──────────┼──────────", 841 + ), 525 842 ); 526 843 527 - const avgResponseTime = 528 - levelStats.responseTimeTotal / levelStats.totalRequests; 529 - console.log( 530 - `${chalk.cyan("Average Response Time:")} ${ 531 - avgResponseTime <= CONFIG.responseTimeThreshold 532 - ? chalk.green(`${avgResponseTime.toFixed(2)} ms`) 533 - : chalk.red(`${avgResponseTime.toFixed(2)} ms`) 534 - }`, 535 - ); 844 + // For each concurrency level tested 845 + for (const result of concurrencyResults) { 846 + const isBreakingPoint = 847 + breakingPoint && result.concurrency === breakingPoint.concurrency; 536 848 537 - console.log(`${chalk.cyan("Total Requests:")} ${levelStats.totalRequests}`); 538 - console.log( 539 - `${chalk.cyan("Successful Requests:")} ${levelStats.successfulRequests}`, 540 - ); 541 - console.log(`${chalk.cyan("Failed Requests:")} ${levelStats.failedRequests}`); 542 - console.log( 543 - `${chalk.cyan("Test Duration:")} ${((levelStats.endTime - levelStats.startTime) / 1000).toFixed(2)}s`, 544 - ); 849 + // Format status based on thresholds 850 + const statusColor = 851 + result.successRate < CONFIG.successThreshold 852 + ? chalk.red 853 + : result.p95ResponseTime > CONFIG.responseTimeThreshold 854 + ? chalk.yellow 855 + : chalk.green; 545 856 546 - // Add memory usage info if available 547 - if (levelStats.memoryUsage) { 548 - console.log( 549 - `${chalk.cyan("Memory RSS:")} ${(levelStats.memoryUsage.rss / 1024 / 1024).toFixed(2)} MB`, 550 - ); 857 + const status = 858 + result.successRate < CONFIG.successThreshold 859 + ? "FAIL" 860 + : result.p95ResponseTime > CONFIG.responseTimeThreshold 861 + ? "SLOW" 862 + : "PASS"; 863 + 864 + // Text color for the entire row 865 + const rowColor = isBreakingPoint ? chalk.bold.red : chalk.white; 866 + 551 867 console.log( 552 - `${chalk.cyan("Heap Used:")} ${(levelStats.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`, 868 + rowColor( 869 + `${result.concurrency.toString().padEnd(10)} | ${result.requestsPerSecond.toFixed(1).padEnd(8)} | ${result.successRate.toFixed(1).padEnd(10)} | ${(result.responseTimeTotal / result.totalRequests).toFixed(1).padEnd(8)} | ${result.p95ResponseTime.toFixed(1).padEnd(8)} | ${result.p99ResponseTime.toFixed(1).padEnd(8)} | ${result.p95TTFB.toFixed(1).padEnd(8)} | ${statusColor(status)}${isBreakingPoint ? " ← BREAKING POINT" : ""}`, 870 + ), 553 871 ); 554 872 } 555 - } 556 873 557 - function printBreakingPointSummary() { 558 - console.log("\n"); 559 - console.log(chalk.bold.magenta("🔥 BREAKING POINT SUMMARY 🔥")); 560 874 console.log( 561 875 chalk.gray("════════════════════════════════════════════════════════"), 562 876 ); 563 877 564 878 if (breakingPoint) { 565 879 console.log( 566 - chalk.bold.yellow( 567 - `Server breaking point: ${breakingPoint.concurrency} concurrent users`, 880 + chalk.yellow( 881 + `⚠️ Breaking point detected at ${chalk.bold(breakingPoint.concurrency)} concurrent users`, 568 882 ), 569 883 ); 570 884 console.log( 571 - chalk.gray("────────────────────────────────────────────────────"), 885 + ` - Success Rate: ${chalk.bold(breakingPoint.successRate.toFixed(2))}% (Threshold: ${CONFIG.successThreshold}%)`, 572 886 ); 573 887 console.log( 574 - `${chalk.cyan("Success Rate:")} ${chalk.red(`${breakingPoint.successRate.toFixed(2)}%`)}`, 888 + ` - p95 Response Time: ${chalk.bold(breakingPoint.p95ResponseTime.toFixed(2))}ms (Threshold: ${CONFIG.responseTimeThreshold}ms)`, 575 889 ); 890 + } else { 891 + const lastConcurrency = 892 + concurrencyResults.length > 0 893 + ? concurrencyResults[concurrencyResults.length - 1]?.concurrency || 0 894 + : 0; 895 + 576 896 console.log( 577 - `${chalk.cyan("Requests per Second:")} ${breakingPoint.requestsPerSecond.toFixed(2)}`, 897 + chalk.green( 898 + `✅ No breaking point detected up to ${chalk.bold(lastConcurrency)} concurrent users`, 899 + ), 578 900 ); 901 + } 579 902 580 - const avgResponseTime = 581 - breakingPoint.responseTimeTotal / breakingPoint.totalRequests; 582 - console.log( 583 - `${chalk.cyan("Average Response Time:")} ${chalk.red(`${avgResponseTime.toFixed(2)} ms`)}`, 903 + // Find the highest RPS level 904 + if (concurrencyResults.length > 0) { 905 + const maxRpsResult = concurrencyResults.reduce((prev, current) => 906 + current.requestsPerSecond > prev.requestsPerSecond ? current : prev, 584 907 ); 585 908 586 - // Get the last successful level 587 - const lastGoodLevelIndex = 588 - concurrencyResults.findIndex( 589 - (stats) => stats.concurrency === breakingPoint.concurrency, 590 - ) - 1; 591 - 592 - if (lastGoodLevelIndex >= 0) { 593 - const safeLevel = concurrencyResults[lastGoodLevelIndex]; 594 - if (safeLevel) { 595 - console.log(""); 596 - console.log( 597 - chalk.bold.green( 598 - `✅ Recommended Safe Concurrency: ${safeLevel.concurrency} users`, 599 - ), 600 - ); 601 - console.log( 602 - `${chalk.cyan("Success Rate:")} ${chalk.green(`${safeLevel.successRate.toFixed(2)}%`)}`, 603 - ); 604 - console.log( 605 - `${chalk.cyan("Requests per Second:")} ${safeLevel.requestsPerSecond.toFixed(2)}`, 606 - ); 909 + console.log( 910 + chalk.green( 911 + `⚡ Peak performance: ${chalk.bold(maxRpsResult.requestsPerSecond.toFixed(2))} requests/second at ${chalk.bold(maxRpsResult.concurrency)} concurrent users`, 912 + ), 913 + ); 914 + } 915 + } 607 916 608 - const safeAvgTime = 609 - safeLevel.responseTimeTotal / safeLevel.totalRequests; 610 - console.log( 611 - `${chalk.cyan("Average Response Time:")} ${chalk.green(`${safeAvgTime.toFixed(2)} ms`)}`, 612 - ); 613 - } 614 - } 615 - } else { 616 - console.log(chalk.bold.green("✅ No breaking point found!")); 917 + // Export results to CSV file 918 + function exportToCsv(): string { 919 + const csvRows: string[] = []; 617 920 618 - if (concurrencyResults.length > 0) { 619 - const maxLevel = concurrencyResults[concurrencyResults.length - 1]; 620 - if (maxLevel) { 621 - console.log( 622 - chalk.gray("────────────────────────────────────────────────────"), 623 - ); 624 - console.log( 625 - chalk.bold.green( 626 - `Maximum tested concurrency: ${maxLevel.concurrency} users`, 627 - ), 628 - ); 629 - console.log( 630 - `${chalk.cyan("Success Rate:")} ${chalk.green(`${maxLevel.successRate.toFixed(2)}%`)}`, 631 - ); 632 - console.log( 633 - `${chalk.cyan("Requests per Second:")} ${maxLevel.requestsPerSecond.toFixed(2)}`, 634 - ); 921 + // Add header row 922 + csvRows.push( 923 + "Concurrency,Requests,Success Rate,Requests/Sec,Avg Time (ms),p50 (ms),p95 (ms),p99 (ms),TTFB p50 (ms),TTFB p95 (ms),TTFB p99 (ms)", 924 + ); 635 925 636 - const maxAvgTime = maxLevel.responseTimeTotal / maxLevel.totalRequests; 637 - console.log( 638 - `${chalk.cyan("Average Response Time:")} ${chalk.green(`${maxAvgTime.toFixed(2)} ms`)}`, 639 - ); 640 - } 641 - } 926 + // Add data rows 927 + for (const result of concurrencyResults) { 928 + csvRows.push( 929 + [ 930 + result.concurrency, 931 + result.totalRequests, 932 + result.successRate.toFixed(2), 933 + result.requestsPerSecond.toFixed(2), 934 + (result.responseTimeTotal / result.totalRequests).toFixed(2), 935 + result.p50ResponseTime.toFixed(2), 936 + result.p95ResponseTime.toFixed(2), 937 + result.p99ResponseTime.toFixed(2), 938 + result.p50TTFB.toFixed(2), 939 + result.p95TTFB.toFixed(2), 940 + result.p99TTFB.toFixed(2), 941 + ].join(","), 942 + ); 642 943 } 643 944 644 - console.log(""); 645 - console.log(chalk.bold.white("📈 Concurrency Progression:")); 945 + // Join all rows with newlines 946 + return csvRows.join("\n"); 947 + } 646 948 647 - for (const levelStats of concurrencyResults) { 648 - const avgResponseTime = 649 - levelStats.responseTimeTotal / levelStats.totalRequests; 949 + // Export detailed results to JSON file 950 + function exportToJson(): string { 951 + return JSON.stringify( 952 + { 953 + config: CONFIG, 954 + results: concurrencyResults, 955 + breakingPoint: breakingPoint, 956 + timestamp: new Date().toISOString(), 957 + }, 958 + null, 959 + 2, 960 + ); 961 + } 650 962 651 - // Determine if this level was successful 652 - const isSuccessful = 653 - levelStats.successRate >= CONFIG.successThreshold && 654 - avgResponseTime <= CONFIG.responseTimeThreshold; 655 - 656 - // Get icon and color based on success 657 - const icon = isSuccessful ? "✅" : "❌"; 658 - const color = isSuccessful ? chalk.green : chalk.red; 963 + // Checks if a test run fails the success criteria 964 + function checkFailureCriteria(result: ConcurrencyStats): boolean { 965 + // Check success rate threshold 966 + if (result.successRate < CONFIG.successThreshold) { 967 + logWithTime( 968 + `Success rate ${result.successRate.toFixed(2)}% is below threshold ${CONFIG.successThreshold}%`, 969 + "warn", 970 + ); 971 + return true; 972 + } 659 973 660 - console.log( 661 - color( 662 - `${icon} ${levelStats.concurrency} users: ${levelStats.successRate.toFixed(2)}% success, ${levelStats.requestsPerSecond.toFixed(2)} req/s, ${avgResponseTime.toFixed(2)}ms avg`, 663 - ), 974 + // Check response time threshold (p95) 975 + if (result.p95ResponseTime > CONFIG.responseTimeThreshold) { 976 + logWithTime( 977 + `p95 response time ${result.p95ResponseTime.toFixed(2)}ms exceeds threshold ${CONFIG.responseTimeThreshold}ms`, 978 + "warn", 664 979 ); 980 + return true; 665 981 } 666 982 667 - console.log(""); 668 - console.log(chalk.gray("HN Front Page Readiness Assessment:")); 983 + return false; 984 + } 985 + 986 + // Save result files 987 + async function saveResultFiles(): Promise<void> { 988 + try { 989 + const timestamp = new Date() 990 + .toISOString() 991 + .replace(/:/g, "-") 992 + .replace(/\./g, "-"); 993 + 994 + // Save CSV results 995 + const csvContent = exportToCsv(); 996 + const csvFilename = `stress-test-results-${timestamp}.csv`; 997 + await Bun.write(csvFilename, csvContent); 998 + logWithTime(`Saved CSV results to ${csvFilename}`, "success"); 669 999 670 - // Hacker News Front Page typically might see ~100-500 concurrent users 671 - if (!breakingPoint || breakingPoint.concurrency > 500) { 672 - console.log( 673 - chalk.bold.green( 674 - "✅ READY FOR HN FRONT PAGE! Your server can handle high traffic loads.", 675 - ), 676 - ); 677 - } else if (breakingPoint.concurrency > 100) { 678 - console.log( 679 - chalk.bold.yellow( 680 - "⚠️ POTENTIALLY READY: Your server may handle the front page but could struggle with peak traffic.", 681 - ), 682 - ); 683 - } else { 684 - console.log( 685 - chalk.bold.red( 686 - "❌ NOT READY: Your server is likely to fail under HN front page traffic.", 687 - ), 1000 + // Save JSON results 1001 + const jsonContent = exportToJson(); 1002 + const jsonFilename = `stress-test-results-${timestamp}.json`; 1003 + await Bun.write(jsonFilename, jsonContent); 1004 + logWithTime(`Saved detailed JSON results to ${jsonFilename}`, "success"); 1005 + } catch (error) { 1006 + logWithTime( 1007 + `Error saving result files: ${(error as Error).message}`, 1008 + "error", 688 1009 ); 689 1010 } 690 1011 } 691 1012 692 - async function main() { 693 - console.clear(); 694 - console.log(chalk.bold.cyan("⚡ Hacker News Breaking Point Stress Test ⚡")); 1013 + // Main test function that runs through increasing concurrency levels 1014 + async function runTest(): Promise<void> { 1015 + console.log(chalk.bold.cyan("🚀 API Stress Test 🚀")); 695 1016 console.log( 696 1017 chalk.gray("════════════════════════════════════════════════════════"), 697 1018 ); 698 - console.log(`${chalk.cyan("Base URL:")} ${chalk.yellow(CONFIG.baseUrl)}`); 1019 + console.log(chalk.cyan(`Base URL: ${CONFIG.baseUrl}`)); 1020 + console.log(chalk.cyan(`Endpoints: ${endpoints.join(", ")}`)); 699 1021 console.log( 700 - `${chalk.cyan("Starting Users:")} ${chalk.yellow(CONFIG.startConcurrency)}`, 1022 + chalk.cyan( 1023 + `Concurrency: ${CONFIG.startConcurrency} to ${CONFIG.maxConcurrency} (×${CONFIG.concurrencyFactor} steps)`, 1024 + ), 701 1025 ); 1026 + console.log(chalk.cyan(`Requests per user: ${CONFIG.requestsPerUser}`)); 1027 + console.log(chalk.cyan(`Success threshold: ${CONFIG.successThreshold}%`)); 702 1028 console.log( 703 - `${chalk.cyan("Maximum Users:")} ${chalk.yellow(CONFIG.maxConcurrency)}`, 1029 + chalk.cyan(`Response time threshold: ${CONFIG.responseTimeThreshold}ms`), 704 1030 ); 705 - console.log( 706 - `${chalk.cyan("Concurrency Factor:")} ${chalk.yellow(CONFIG.concurrencyFactor)}x (exponential growth)`, 707 - ); 708 - console.log( 709 - `${chalk.cyan("Success Threshold:")} ${chalk.yellow(CONFIG.successThreshold)}%`, 710 - ); 711 - console.log( 712 - `${chalk.cyan("Response Time Threshold:")} ${chalk.yellow(CONFIG.responseTimeThreshold)}ms`, 713 - ); 714 - console.log( 715 - `${chalk.cyan("Caching:")} ${CONFIG.runWithCaching ? chalk.green("Enabled") : chalk.red("Disabled")}`, 716 - ); 717 - console.log( 718 - `${chalk.cyan("Detailed Logging:")} ${!CONFIG.disableDetailedLogging ? chalk.green("Enabled") : chalk.yellow("Disabled")}`, 719 - ); 1031 + 1032 + if (CONFIG.runWithCaching) { 1033 + console.log(chalk.blue("ℹ️ Caching enabled (using ETags)")); 1034 + } else { 1035 + console.log(chalk.yellow("⚠️ Caching disabled (no ETags)")); 1036 + } 1037 + 720 1038 console.log( 721 1039 chalk.gray("════════════════════════════════════════════════════════"), 722 1040 ); 723 1041 724 - // Verify server is up 725 - const spinner = new Spinner("Checking server availability..."); 726 - spinner.start(); 1042 + // Warm up the server first 1043 + await warmupServer(); 1044 + 1045 + // Start with the initial concurrency level 1046 + let concurrencyLevel = CONFIG.startConcurrency; 727 1047 728 - try { 729 - const response = await fetch(`${CONFIG.baseUrl}/health`); 730 - if (!response.ok) { 731 - spinner.stop(); 1048 + // Keep testing until we hit the max concurrency or a breaking point 1049 + while (concurrencyLevel <= CONFIG.maxConcurrency) { 1050 + // Run the test at this concurrency level 1051 + const result = await runConcurrencyLevel(concurrencyLevel); 1052 + 1053 + // Store the result 1054 + concurrencyResults.push(result); 1055 + 1056 + // Print brief stats for this level 1057 + logWithTime( 1058 + `Completed level: ${concurrencyLevel} users, ` + 1059 + `RPS: ${result.requestsPerSecond.toFixed(2)}, ` + 1060 + `Success: ${result.successRate.toFixed(2)}%, ` + 1061 + `Avg: ${(result.responseTimeTotal / result.totalRequests).toFixed(2)}ms, ` + 1062 + `p95: ${result.p95ResponseTime.toFixed(2)}ms`, 1063 + "success", 1064 + ); 1065 + 1066 + // Check if we should stop 1067 + if (CONFIG.stopOnFailure && checkFailureCriteria(result)) { 1068 + breakingPoint = result; 732 1069 logWithTime( 733 - `Server health check failed: ${response.status} ${response.statusText}`, 734 - "error", 1070 + `Breaking point reached at ${concurrencyLevel} concurrent users`, 1071 + "warn", 735 1072 ); 736 - process.exit(1); 1073 + break; 737 1074 } 738 1075 739 - spinner.stop(); 740 - logWithTime("Server is up and running", "success"); 741 - } catch (error) { 742 - spinner.stop(); 743 - logWithTime(`Server not available: ${(error as Error).message}`, "error"); 744 - logWithTime( 745 - "Make sure the server is running before starting the stress test", 746 - "info", 747 - ); 748 - process.exit(1); 1076 + // Increase concurrency level 1077 + concurrencyLevel = Math.round(concurrencyLevel * CONFIG.concurrencyFactor); 1078 + 1079 + // Add a delay between levels 1080 + if (concurrencyLevel <= CONFIG.maxConcurrency) { 1081 + logWithTime( 1082 + `Waiting ${CONFIG.delayBetweenLevels / 1000} seconds before next level...`, 1083 + "info", 1084 + ); 1085 + await new Promise((resolve) => 1086 + setTimeout(resolve, CONFIG.delayBetweenLevels), 1087 + ); 1088 + } 749 1089 } 750 1090 751 - console.log("\n"); 752 - logWithTime("Starting breaking point test...", "info"); 753 - 754 - let currentConcurrency = CONFIG.startConcurrency; 755 - let failureDetected = false; 1091 + // Print final results 1092 + printConcurrencySummary(); 1093 + printResults(); 756 1094 757 - while (currentConcurrency <= CONFIG.maxConcurrency && !failureDetected) { 758 - // Run test with current concurrency level 759 - const levelResults = await runConcurrencyLevel(currentConcurrency); 1095 + // Save result files 1096 + await saveResultFiles(); 1097 + } 760 1098 761 - // Store results 762 - concurrencyResults.push(levelResults); 1099 + // Check args for custom config overrides 1100 + function parseCliArgs(): void { 1101 + const args = process.argv.slice(2); 763 1102 764 - // Print results for this level 765 - printLevelResults(levelResults); 1103 + for (let i = 0; i < args.length; i++) { 1104 + const arg = args[i]; 766 1105 767 - // Check if this is the breaking point 768 - const avgResponseTime = 769 - levelResults.responseTimeTotal / levelResults.totalRequests; 770 - if ( 771 - levelResults.successRate < CONFIG.successThreshold || 772 - avgResponseTime > CONFIG.responseTimeThreshold 773 - ) { 774 - breakingPoint = levelResults; 1106 + if (!arg) continue; 775 1107 776 - if (CONFIG.stopOnFailure) { 777 - logWithTime( 778 - `Breaking point found at ${currentConcurrency} users!`, 779 - "warn", 780 - ); 781 - failureDetected = true; 782 - } else { 783 - logWithTime( 784 - `Performance degradation at ${currentConcurrency} users, but continuing test...`, 785 - "warn", 786 - ); 787 - } 788 - } 1108 + // Check for configuration overrides 1109 + if (arg.startsWith("--")) { 1110 + const configKey = arg 1111 + .slice(2) 1112 + .replace(/-([a-z])/g, (g) => g[1]?.toUpperCase() || ""); 1113 + const configValue = args[i + 1]; 789 1114 790 - // Increment concurrency exponentially for next level 791 - currentConcurrency = Math.floor( 792 - currentConcurrency * CONFIG.concurrencyFactor, 793 - ); 1115 + if (configValue && !configValue.startsWith("--")) { 1116 + try { 1117 + // Convert numeric strings or booleans 1118 + if (/^\d+$/.test(configValue)) { 1119 + (CONFIG as Record<string, unknown>)[configKey] = Number.parseInt( 1120 + configValue, 1121 + 10, 1122 + ); 1123 + } else if (/^\d+\.\d+$/.test(configValue)) { 1124 + (CONFIG as Record<string, unknown>)[configKey] = 1125 + Number.parseFloat(configValue); 1126 + } else if (configValue === "true" || configValue === "false") { 1127 + (CONFIG as Record<string, unknown>)[configKey] = 1128 + configValue === "true"; 1129 + } else { 1130 + (CONFIG as Record<string, unknown>)[configKey] = configValue; 1131 + } 794 1132 795 - // Wait between levels 796 - if (!failureDetected && currentConcurrency <= CONFIG.maxConcurrency) { 797 - await new Promise((resolve) => 798 - setTimeout(resolve, CONFIG.delayBetweenLevels), 799 - ); 1133 + logWithTime(`Config override: ${configKey} = ${configValue}`, "info"); 1134 + i++; // Skip the value 1135 + } catch (e) { 1136 + logWithTime( 1137 + `Error parsing config value for ${configKey}: ${e}`, 1138 + "error", 1139 + ); 1140 + } 1141 + } 800 1142 } 801 1143 } 1144 + } 802 1145 803 - // Print final summary 804 - printBreakingPointSummary(); 1146 + // Entry point 1147 + async function main(): Promise<void> { 1148 + try { 1149 + // Parse CLI arguments 1150 + parseCliArgs(); 805 1151 806 - if (!breakingPoint && currentConcurrency > CONFIG.maxConcurrency) { 807 - logWithTime( 808 - "Maximum concurrency level reached without hitting breaking point.", 809 - "success", 810 - ); 811 - logWithTime( 812 - `Your server can handle at least ${CONFIG.maxConcurrency} concurrent users!`, 813 - "success", 814 - ); 1152 + // Run the test 1153 + await runTest(); 1154 + } catch (error) { 1155 + logWithTime(`Stress test failed: ${(error as Error).message}`, "error"); 1156 + console.error(error); 1157 + process.exit(1); 815 1158 } 816 1159 } 817 1160 818 - // Run the stress test and handle errors 819 - main().catch((error) => { 820 - console.error(`${chalk.red("Fatal error:")} ${error.message}`); 821 - process.exit(1); 822 - }); 1161 + // Start the test 1162 + main();