this repo has no description
3
fork

Configure Feed

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

feat: add stress test tool

+828 -1
+3
bun.lock
··· 8 8 "@sentry/bun": "^9.10.1", 9 9 "@types/pg": "^8.11.13", 10 10 "bottleneck": "^2.19.5", 11 + "chalk": "^5.4.1", 11 12 "colors": "^1.4.0", 12 13 "cron": "^4.3.0", 13 14 "drizzle-orm": "^0.42.0", ··· 212 213 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], 213 214 214 215 "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="], 216 + 217 + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], 215 218 216 219 "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], 217 220
+3 -1
package.json
··· 12 12 "db:generate": "drizzle-kit generate", 13 13 "db:migrate": "drizzle-kit migrate", 14 14 "db:studio": "drizzle-kit studio --port 3001", 15 - "db:push": "drizzle-kit push" 15 + "db:push": "drizzle-kit push", 16 + "stress": "bun run stress.ts" 16 17 }, 17 18 "devDependencies": { 18 19 "@types/bun": "latest", ··· 26 27 "@sentry/bun": "^9.10.1", 27 28 "@types/pg": "^8.11.13", 28 29 "bottleneck": "^2.19.5", 30 + "chalk": "^5.4.1", 29 31 "colors": "^1.4.0", 30 32 "cron": "^4.3.0", 31 33 "drizzle-orm": "^0.42.0",
+822
stress.ts
··· 1 + import { Chalk } from "chalk"; 2 + import { randomUUIDv7 } from "bun"; 3 + 4 + // Create a console logger with fancy colors 5 + const chalk = new Chalk({ level: 3 }); 6 + const endpoints = [ 7 + "/api/stories", 8 + "/api/stats/total-stories", 9 + "/api/stats/verified-users", 10 + ]; 11 + 12 + // Script configuration 13 + const CONFIG = { 14 + 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 26 + }; 27 + 28 + // Stats tracking 29 + type EndpointStats = { 30 + totalRequests: number; 31 + successfulRequests: number; 32 + notModifiedResponses: number; 33 + failedRequests: number; 34 + responseTimeTotal: number; 35 + responseTimeMin: number; 36 + responseTimeMax: number; 37 + }; 38 + 39 + // Add memory usage tracking 40 + type ConcurrencyStats = { 41 + concurrency: number; 42 + totalRequests: number; 43 + successfulRequests: number; 44 + notModifiedResponses: number; 45 + failedRequests: number; 46 + responseTimeTotal: number; 47 + responseTimeMin: number; 48 + responseTimeMax: number; 49 + startTime: number; 50 + endTime: number; 51 + userCompletedCount: number; 52 + requestsPerSecond: number; 53 + successRate: number; 54 + endpoints: Record<string, EndpointStats>; 55 + memoryUsage?: { 56 + rss: number; 57 + heapTotal: number; 58 + heapUsed: number; 59 + external: number; 60 + }; 61 + }; 62 + 63 + const concurrencyResults: ConcurrencyStats[] = []; 64 + let breakingPoint: ConcurrencyStats | null = null; 65 + 66 + // Current level stats 67 + const stats = { 68 + concurrency: 0, 69 + totalRequests: 0, 70 + successfulRequests: 0, 71 + notModifiedResponses: 0, 72 + failedRequests: 0, 73 + responseTimeTotal: 0, 74 + responseTimeMin: Number.MAX_VALUE, 75 + responseTimeMax: 0, 76 + startTime: 0, 77 + endTime: 0, 78 + userCompletedCount: 0, 79 + requestsPerSecond: 0, 80 + successRate: 0, 81 + endpoints: {} as Record<string, EndpointStats>, 82 + }; 83 + 84 + // Initialize stats for each endpoint 85 + for (const endpoint of endpoints) { 86 + stats.endpoints[endpoint] = { 87 + totalRequests: 0, 88 + successfulRequests: 0, 89 + notModifiedResponses: 0, 90 + failedRequests: 0, 91 + responseTimeTotal: 0, 92 + responseTimeMin: Number.MAX_VALUE, 93 + responseTimeMax: 0, 94 + }; 95 + } 96 + // ETag cache 97 + const etagCache: Record<string, string> = {}; 98 + 99 + // Spinner for loading animation 100 + class Spinner { 101 + private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 102 + private interval: NodeJS.Timeout | null = null; 103 + private currentFrame = 0; 104 + private text: string; 105 + 106 + constructor(text: string) { 107 + this.text = text; 108 + } 109 + 110 + start() { 111 + this.interval = setInterval(() => { 112 + process.stdout.write( 113 + `\r${chalk.cyan(this.frames[this.currentFrame])} ${this.text}`, 114 + ); 115 + this.currentFrame = (this.currentFrame + 1) % this.frames.length; 116 + }, 80); 117 + } 118 + 119 + stop() { 120 + if (this.interval) { 121 + clearInterval(this.interval); 122 + this.interval = null; 123 + process.stdout.write( 124 + "\r \r", 125 + ); 126 + } 127 + } 128 + 129 + setText(text: string) { 130 + this.text = text; 131 + } 132 + } 133 + 134 + // Helper to log with timestamp 135 + function logWithTime( 136 + message: string, 137 + type: "info" | "success" | "error" | "warn" = "info", 138 + ) { 139 + const timestamp = new Date().toISOString().split("T")[1]?.slice(0, -1) || ""; 140 + const prefix = { 141 + info: chalk.blue(`[${timestamp}] ℹ️ `), 142 + success: chalk.green(`[${timestamp}] ✅ `), 143 + error: chalk.red(`[${timestamp}] ❌ `), 144 + warn: chalk.yellow(`[${timestamp}] ⚠️ `), 145 + }[type]; 146 + 147 + console.log(`${prefix}${message}`); 148 + } 149 + 150 + // Make a HTTP request with timing 151 + async function makeRequest( 152 + endpoint: string, 153 + userId: string, 154 + requestId: number, 155 + ): Promise<void> { 156 + const url = `${CONFIG.baseUrl}${endpoint}`; 157 + const headers: Record<string, string> = { 158 + "User-Agent": `stress-test-user-${userId}/request-${requestId}`, 159 + }; 160 + 161 + // Add ETag if available and caching is enabled 162 + const cacheKey = `${userId}-${endpoint}`; 163 + if (CONFIG.runWithCaching && etagCache[cacheKey]) { 164 + headers["If-None-Match"] = etagCache[cacheKey]; 165 + } 166 + 167 + try { 168 + const startTime = performance.now(); 169 + const response = await fetch(url, { headers }); 170 + const endTime = performance.now(); 171 + const responseTime = endTime - startTime; 172 + 173 + // Track overall stats 174 + stats.totalRequests++; 175 + 176 + // Ensure the endpoint exists in stats.endpoints 177 + if (!stats.endpoints[endpoint]) { 178 + stats.endpoints[endpoint] = { 179 + totalRequests: 0, 180 + successfulRequests: 0, 181 + notModifiedResponses: 0, 182 + failedRequests: 0, 183 + responseTimeTotal: 0, 184 + responseTimeMin: Number.MAX_VALUE, 185 + responseTimeMax: 0, 186 + }; 187 + } 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 + 194 + // Track endpoint-specific stats 195 + stats.endpoints[endpoint].responseTimeTotal += responseTime; 196 + stats.endpoints[endpoint].responseTimeMin = Math.min( 197 + stats.endpoints[endpoint].responseTimeMin, 198 + responseTime, 199 + ); 200 + stats.endpoints[endpoint].responseTimeMax = Math.max( 201 + stats.endpoints[endpoint].responseTimeMax, 202 + responseTime, 203 + ); 204 + 205 + if (response.status === 304) { 206 + stats.notModifiedResponses++; 207 + stats.endpoints[endpoint].notModifiedResponses++; 208 + if (!CONFIG.disableDetailedLogging) { 209 + logWithTime( 210 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - 304 Not Modified (${responseTime.toFixed(2)}ms)`, 211 + "info", 212 + ); 213 + } 214 + } else if (response.ok) { 215 + stats.successfulRequests++; 216 + stats.endpoints[endpoint].successfulRequests++; 217 + if (!CONFIG.disableDetailedLogging) { 218 + logWithTime( 219 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${response.status} OK (${responseTime.toFixed(2)}ms)`, 220 + "success", 221 + ); 222 + } 223 + 224 + // Store ETag for future requests if caching is enabled 225 + if (CONFIG.runWithCaching) { 226 + const etag = response.headers.get("ETag"); 227 + if (etag) { 228 + etagCache[cacheKey] = etag; 229 + } 230 + } 231 + 232 + // Parse JSON response (but don't do anything with it) 233 + await response.json(); 234 + } else { 235 + stats.failedRequests++; 236 + stats.endpoints[endpoint].failedRequests++; 237 + // Always log errors, even if detailed logging is disabled 238 + logWithTime( 239 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - ${response.status} Error (${responseTime.toFixed(2)}ms)`, 240 + "error", 241 + ); 242 + } 243 + } catch (error) { 244 + stats.failedRequests++; 245 + 246 + // Ensure the endpoint exists in stats.endpoints 247 + if (!stats.endpoints[endpoint]) { 248 + stats.endpoints[endpoint] = { 249 + totalRequests: 0, 250 + successfulRequests: 0, 251 + notModifiedResponses: 0, 252 + failedRequests: 0, 253 + responseTimeTotal: 0, 254 + responseTimeMin: Number.MAX_VALUE, 255 + responseTimeMax: 0, 256 + }; 257 + } 258 + 259 + stats.endpoints[endpoint].failedRequests++; 260 + // Always log errors, even if detailed logging is disabled 261 + logWithTime( 262 + `User ${userId.slice(0, 4)} - Request ${requestId} - ${endpoint} - Exception: ${(error as Error).message}`, 263 + "error", 264 + ); 265 + } 266 + } 267 + 268 + // Simulate a user session 269 + async function simulateUser(userId: string): Promise<void> { 270 + try { 271 + // Create all requests at once for maximum concurrency 272 + const requests: Promise<void>[] = []; 273 + 274 + for (let i = 0; i < CONFIG.requestsPerUser; i++) { 275 + // Choose a random endpoint 276 + const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)]; 277 + 278 + // Make sure endpoint is not undefined before adding it 279 + if (endpoint) { 280 + // Instead of waiting for each request, push them to an array 281 + requests.push(makeRequest(endpoint, userId, i + 1)); 282 + 283 + // Add a minimal delay if configured (usually 0) 284 + if (CONFIG.delayBetweenRequests > 0) { 285 + await new Promise((resolve) => 286 + setTimeout(resolve, CONFIG.delayBetweenRequests), 287 + ); 288 + } 289 + } 290 + } 291 + 292 + // Wait for all requests to complete 293 + await Promise.allSettled(requests); 294 + } catch (error) { 295 + logWithTime( 296 + `User ${userId.slice(0, 4)} - Error: ${(error as Error).message}`, 297 + "error", 298 + ); 299 + } finally { 300 + // Mark user as completed regardless of success/failure 301 + stats.userCompletedCount++; 302 + } 303 + } 304 + 305 + // Print results in a fancy way 306 + function printResults() { 307 + console.log("\n"); 308 + console.log(chalk.bold.cyan("🚀 Stress Test Results 🚀")); 309 + console.log( 310 + chalk.gray("════════════════════════════════════════════════════════"), 311 + ); 312 + console.log(chalk.bold.white("📊 General Stats:")); 313 + console.log( 314 + `${chalk.cyan("Total Users:")} ${chalk.yellow(stats.concurrency)}`, 315 + ); 316 + console.log( 317 + `${chalk.cyan("Completed Users:")} ${chalk.yellow(stats.userCompletedCount)}`, 318 + ); 319 + console.log( 320 + `${chalk.cyan("Total Requests:")} ${chalk.yellow(stats.totalRequests)}`, 321 + ); 322 + console.log( 323 + `${chalk.cyan("Successful Requests:")} ${chalk.green(stats.successfulRequests)} (${( 324 + (stats.successfulRequests / stats.totalRequests) * 325 + 100 326 + ).toFixed(2)}%)`, 327 + ); 328 + console.log( 329 + `${chalk.cyan("Not Modified (304):")} ${chalk.blue(stats.notModifiedResponses)} (${((stats.notModifiedResponses / stats.totalRequests) * 100).toFixed(2)}%)`, 330 + ); 331 + console.log( 332 + `${chalk.cyan("Failed Requests:")} ${chalk.red(stats.failedRequests)} (${((stats.failedRequests / stats.totalRequests) * 100).toFixed(2)}%)`, 333 + ); 334 + 335 + const durationInSeconds = (stats.endTime - stats.startTime) / 1000; 336 + console.log( 337 + `${chalk.cyan("Test Duration:")} ${chalk.yellow(durationInSeconds.toFixed(2))} seconds`, 338 + ); 339 + console.log( 340 + `${chalk.cyan("Requests per Second:")} ${chalk.yellow((stats.totalRequests / durationInSeconds).toFixed(2))}`, 341 + ); 342 + 343 + const avgResponseTime = stats.responseTimeTotal / stats.totalRequests; 344 + console.log( 345 + `${chalk.cyan("Average Response Time:")} ${chalk.yellow(avgResponseTime.toFixed(2))} ms`, 346 + ); 347 + console.log( 348 + `${chalk.cyan("Min Response Time:")} ${chalk.green(stats.responseTimeMin.toFixed(2))} ms`, 349 + ); 350 + console.log( 351 + `${chalk.cyan("Max Response Time:")} ${chalk.red(stats.responseTimeMax.toFixed(2))} ms`, 352 + ); 353 + 354 + console.log("\n"); 355 + console.log(chalk.bold.white("📈 Endpoint Stats:")); 356 + 357 + for (const [endpoint, endpointStats] of Object.entries(stats.endpoints)) { 358 + if (endpointStats.totalRequests === 0) continue; 359 + 360 + console.log( 361 + chalk.gray("────────────────────────────────────────────────────"), 362 + ); 363 + console.log(chalk.bold.cyan(`Endpoint: ${endpoint}`)); 364 + console.log( 365 + `${chalk.cyan("Total Requests:")} ${chalk.yellow(endpointStats.totalRequests)}`, 366 + ); 367 + console.log( 368 + `${chalk.cyan("Successful Requests:")} ${chalk.green(endpointStats.successfulRequests)} (${((endpointStats.successfulRequests / endpointStats.totalRequests) * 100).toFixed(2)}%)`, 369 + ); 370 + console.log( 371 + `${chalk.cyan("Not Modified (304):")} ${chalk.blue(endpointStats.notModifiedResponses)} (${((endpointStats.notModifiedResponses / endpointStats.totalRequests) * 100).toFixed(2)}%)`, 372 + ); 373 + console.log( 374 + `${chalk.cyan("Failed Requests:")} ${chalk.red(endpointStats.failedRequests)} (${((endpointStats.failedRequests / endpointStats.totalRequests) * 100).toFixed(2)}%)`, 375 + ); 376 + 377 + const avgResponseTime = 378 + endpointStats.responseTimeTotal / endpointStats.totalRequests; 379 + console.log( 380 + `${chalk.cyan("Average Response Time:")} ${chalk.yellow(avgResponseTime.toFixed(2))} ms`, 381 + ); 382 + console.log( 383 + `${chalk.cyan("Min Response Time:")} ${chalk.green(endpointStats.responseTimeMin.toFixed(2))} ms`, 384 + ); 385 + console.log( 386 + `${chalk.cyan("Max Response Time:")} ${chalk.red(endpointStats.responseTimeMax.toFixed(2))} ms`, 387 + ); 388 + } 389 + 390 + console.log( 391 + chalk.gray("════════════════════════════════════════════════════════"), 392 + ); 393 + console.log(chalk.bold.green("✅ Stress Test Completed")); 394 + if (CONFIG.runWithCaching) { 395 + console.log(chalk.bold.blue("ℹ️ Test ran with caching enabled (ETags)")); 396 + } else { 397 + console.log(chalk.bold.yellow("⚠️ Test ran without caching (no ETags)")); 398 + } 399 + } 400 + 401 + // Main function 402 + async function runConcurrencyLevel( 403 + concurrencyLevel: number, 404 + ): Promise<ConcurrencyStats> { 405 + // Reset stats for this level 406 + Object.assign(stats, { 407 + concurrency: concurrencyLevel, 408 + totalRequests: 0, 409 + successfulRequests: 0, 410 + notModifiedResponses: 0, 411 + failedRequests: 0, 412 + responseTimeTotal: 0, 413 + responseTimeMin: Number.MAX_VALUE, 414 + responseTimeMax: 0, 415 + startTime: 0, 416 + endTime: 0, 417 + userCompletedCount: 0, 418 + requestsPerSecond: 0, 419 + successRate: 0, 420 + endpoints: {}, 421 + }); 422 + 423 + // Reset endpoint stats 424 + for (const endpoint of endpoints) { 425 + stats.endpoints[endpoint] = { 426 + totalRequests: 0, 427 + successfulRequests: 0, 428 + notModifiedResponses: 0, 429 + failedRequests: 0, 430 + responseTimeTotal: 0, 431 + responseTimeMin: Number.MAX_VALUE, 432 + responseTimeMax: 0, 433 + }; 434 + } 435 + 436 + logWithTime(`Running concurrency level: ${concurrencyLevel} users`, "info"); 437 + stats.startTime = performance.now(); 438 + 439 + // Create user promises 440 + const userPromises: Promise<void>[] = []; 441 + 442 + for (let i = 0; i < concurrencyLevel; i++) { 443 + const userId = randomUUIDv7(); 444 + userPromises.push(simulateUser(userId)); 445 + } 446 + 447 + // Wait for all users to complete 448 + const spinner = new Spinner( 449 + `Running ${concurrencyLevel} concurrent users...`, 450 + ); 451 + spinner.start(); 452 + 453 + // Only update spinner occasionally to reduce logging overhead 454 + const updateIntervalMs = concurrencyLevel > 10000 ? 500 : 100; 455 + 456 + let lastCount = 0; 457 + const updateInterval = setInterval(() => { 458 + if (stats.userCompletedCount > lastCount) { 459 + lastCount = stats.userCompletedCount; 460 + // Only update text if significant progress has been made 461 + if ( 462 + stats.userCompletedCount === concurrencyLevel || 463 + stats.userCompletedCount % 464 + Math.max(1, Math.floor(concurrencyLevel / 20)) === 465 + 0 466 + ) { 467 + spinner.setText( 468 + `Progress: ${stats.userCompletedCount}/${concurrencyLevel} users (${Math.floor((stats.userCompletedCount / concurrencyLevel) * 100)}%)`, 469 + ); 470 + } 471 + } 472 + }, updateIntervalMs); 473 + 474 + await Promise.allSettled(userPromises); 475 + 476 + clearInterval(updateInterval); 477 + spinner.stop(); 478 + 479 + stats.endTime = performance.now(); 480 + 481 + // Calculate final stats 482 + const durationInSeconds = (stats.endTime - stats.startTime) / 1000; 483 + stats.requestsPerSecond = stats.totalRequests / durationInSeconds; 484 + stats.successRate = 485 + stats.totalRequests > 0 486 + ? (stats.successfulRequests / stats.totalRequests) * 100 487 + : 0; 488 + 489 + // Capture memory usage 490 + if (process.memoryUsage) { 491 + const memoryUsage = process.memoryUsage(); 492 + (stats as any).memoryUsage = { 493 + rss: memoryUsage.rss, 494 + heapTotal: memoryUsage.heapTotal, 495 + heapUsed: memoryUsage.heapUsed, 496 + external: memoryUsage.external, 497 + }; 498 + } 499 + 500 + // Create a deep copy of the stats to return 501 + const result: ConcurrencyStats = JSON.parse(JSON.stringify(stats)); 502 + 503 + return result; 504 + } 505 + 506 + function printLevelResults(levelStats: ConcurrencyStats) { 507 + console.log("\n"); 508 + console.log( 509 + chalk.bold.cyan(`📊 Concurrency Level: ${levelStats.concurrency} users`), 510 + ); 511 + console.log( 512 + chalk.gray("────────────────────────────────────────────────────"), 513 + ); 514 + 515 + 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 + }`, 521 + ); 522 + 523 + console.log( 524 + `${chalk.cyan("Requests per Second:")} ${levelStats.requestsPerSecond.toFixed(2)}`, 525 + ); 526 + 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 + ); 536 + 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 + ); 545 + 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 + ); 551 + console.log( 552 + `${chalk.cyan("Heap Used:")} ${(levelStats.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`, 553 + ); 554 + } 555 + } 556 + 557 + function printBreakingPointSummary() { 558 + console.log("\n"); 559 + console.log(chalk.bold.magenta("🔥 BREAKING POINT SUMMARY 🔥")); 560 + console.log( 561 + chalk.gray("════════════════════════════════════════════════════════"), 562 + ); 563 + 564 + if (breakingPoint) { 565 + console.log( 566 + chalk.bold.yellow( 567 + `Server breaking point: ${breakingPoint.concurrency} concurrent users`, 568 + ), 569 + ); 570 + console.log( 571 + chalk.gray("────────────────────────────────────────────────────"), 572 + ); 573 + console.log( 574 + `${chalk.cyan("Success Rate:")} ${chalk.red(`${breakingPoint.successRate.toFixed(2)}%`)}`, 575 + ); 576 + console.log( 577 + `${chalk.cyan("Requests per Second:")} ${breakingPoint.requestsPerSecond.toFixed(2)}`, 578 + ); 579 + 580 + const avgResponseTime = 581 + breakingPoint.responseTimeTotal / breakingPoint.totalRequests; 582 + console.log( 583 + `${chalk.cyan("Average Response Time:")} ${chalk.red(`${avgResponseTime.toFixed(2)} ms`)}`, 584 + ); 585 + 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 + ); 607 + 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!")); 617 + 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 + ); 635 + 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 + } 642 + } 643 + 644 + console.log(""); 645 + console.log(chalk.bold.white("📈 Concurrency Progression:")); 646 + 647 + for (const levelStats of concurrencyResults) { 648 + const avgResponseTime = 649 + levelStats.responseTimeTotal / levelStats.totalRequests; 650 + 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; 659 + 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 + ), 664 + ); 665 + } 666 + 667 + console.log(""); 668 + console.log(chalk.gray("HN Front Page Readiness Assessment:")); 669 + 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 + ), 688 + ); 689 + } 690 + } 691 + 692 + async function main() { 693 + console.clear(); 694 + console.log(chalk.bold.cyan("⚡ Hacker News Breaking Point Stress Test ⚡")); 695 + console.log( 696 + chalk.gray("════════════════════════════════════════════════════════"), 697 + ); 698 + console.log(`${chalk.cyan("Base URL:")} ${chalk.yellow(CONFIG.baseUrl)}`); 699 + console.log( 700 + `${chalk.cyan("Starting Users:")} ${chalk.yellow(CONFIG.startConcurrency)}`, 701 + ); 702 + console.log( 703 + `${chalk.cyan("Maximum Users:")} ${chalk.yellow(CONFIG.maxConcurrency)}`, 704 + ); 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 + ); 720 + console.log( 721 + chalk.gray("════════════════════════════════════════════════════════"), 722 + ); 723 + 724 + // Verify server is up 725 + const spinner = new Spinner("Checking server availability..."); 726 + spinner.start(); 727 + 728 + try { 729 + const response = await fetch(`${CONFIG.baseUrl}/health`); 730 + if (!response.ok) { 731 + spinner.stop(); 732 + logWithTime( 733 + `Server health check failed: ${response.status} ${response.statusText}`, 734 + "error", 735 + ); 736 + process.exit(1); 737 + } 738 + 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); 749 + } 750 + 751 + console.log("\n"); 752 + logWithTime("Starting breaking point test...", "info"); 753 + 754 + let currentConcurrency = CONFIG.startConcurrency; 755 + let failureDetected = false; 756 + 757 + while (currentConcurrency <= CONFIG.maxConcurrency && !failureDetected) { 758 + // Run test with current concurrency level 759 + const levelResults = await runConcurrencyLevel(currentConcurrency); 760 + 761 + // Store results 762 + concurrencyResults.push(levelResults); 763 + 764 + // Print results for this level 765 + printLevelResults(levelResults); 766 + 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; 775 + 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 + } 789 + 790 + // Increment concurrency exponentially for next level 791 + currentConcurrency = Math.floor( 792 + currentConcurrency * CONFIG.concurrencyFactor, 793 + ); 794 + 795 + // Wait between levels 796 + if (!failureDetected && currentConcurrency <= CONFIG.maxConcurrency) { 797 + await new Promise((resolve) => 798 + setTimeout(resolve, CONFIG.delayBetweenLevels), 799 + ); 800 + } 801 + } 802 + 803 + // Print final summary 804 + printBreakingPointSummary(); 805 + 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 + ); 815 + } 816 + } 817 + 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 + });