307 lines
9.1 KiB
TypeScript
307 lines
9.1 KiB
TypeScript
/**
|
||
* Performance Test Suite
|
||
* Phase 3: Testing & Validation
|
||
*
|
||
* Measures response times, throughput, and identifies bottlenecks
|
||
*/
|
||
|
||
interface PerformanceMetric {
|
||
endpoint: string;
|
||
method: string;
|
||
avgTime: number;
|
||
minTime: number;
|
||
maxTime: number;
|
||
p95Time: number;
|
||
p99Time: number;
|
||
requestsPerSecond: number;
|
||
}
|
||
|
||
const BASE_URL = "http://localhost:5173";
|
||
const metrics: PerformanceMetric[] = [];
|
||
|
||
// Helper to measure request time
|
||
async function measureRequest(
|
||
method: string,
|
||
endpoint: string,
|
||
body?: any,
|
||
): Promise<number> {
|
||
const start = performance.now();
|
||
try {
|
||
const options: RequestInit = {
|
||
method,
|
||
headers: { "Content-Type": "application/json" },
|
||
};
|
||
if (body) options.body = JSON.stringify(body);
|
||
|
||
await fetch(`${BASE_URL}${endpoint}`, options);
|
||
return performance.now() - start;
|
||
} catch {
|
||
return performance.now() - start;
|
||
}
|
||
}
|
||
|
||
// Run multiple requests and collect metrics
|
||
async function benchmarkEndpoint(
|
||
method: string,
|
||
endpoint: string,
|
||
numRequests: number = 50,
|
||
body?: any,
|
||
): Promise<PerformanceMetric> {
|
||
console.log(
|
||
` Benchmarking ${method.padEnd(6)} ${endpoint.padEnd(35)} (${numRequests} requests)...`,
|
||
);
|
||
|
||
const times: number[] = [];
|
||
const startTime = Date.now();
|
||
|
||
for (let i = 0; i < numRequests; i++) {
|
||
const time = await measureRequest(method, endpoint, body);
|
||
times.push(time);
|
||
}
|
||
|
||
const elapsed = Date.now() - startTime;
|
||
times.sort((a, b) => a - b);
|
||
|
||
const metric: PerformanceMetric = {
|
||
endpoint,
|
||
method,
|
||
avgTime: times.reduce((a, b) => a + b) / times.length,
|
||
minTime: times[0],
|
||
maxTime: times[times.length - 1],
|
||
p95Time: times[Math.floor(times.length * 0.95)],
|
||
p99Time: times[Math.floor(times.length * 0.99)],
|
||
requestsPerSecond: (numRequests / elapsed) * 1000,
|
||
};
|
||
|
||
metrics.push(metric);
|
||
return metric;
|
||
}
|
||
|
||
async function runPerformanceTests() {
|
||
console.log("⚡ Performance Test Suite\n");
|
||
|
||
// CATEGORY 1: GET Endpoints (Read-Heavy)
|
||
console.log("\n📊 Category 1: GET Endpoints (Read Performance)");
|
||
console.log("=".repeat(70));
|
||
|
||
console.log("\nBrowse endpoints (pagination-heavy):");
|
||
await benchmarkEndpoint("GET", "/api/creators?page=1&limit=20", 50);
|
||
await benchmarkEndpoint("GET", "/api/opportunities?page=1&limit=20", 50);
|
||
await benchmarkEndpoint("GET", "/api/applications?user_id=test-user", 40);
|
||
|
||
console.log("\nFilter endpoints (filtered queries):");
|
||
await benchmarkEndpoint("GET", "/api/creators?arm=gameforge", 50);
|
||
await benchmarkEndpoint(
|
||
"GET",
|
||
"/api/opportunities?arm=gameforge&sort=recent",
|
||
50,
|
||
);
|
||
await benchmarkEndpoint("GET", "/api/creators?search=test", 40);
|
||
|
||
console.log("\nIndividual resource retrieval:");
|
||
await benchmarkEndpoint("GET", "/api/creators/test_user", 50);
|
||
await benchmarkEndpoint("GET", "/api/opportunities/test-opp-id", 40);
|
||
await benchmarkEndpoint("GET", "/api/devconnect/link?user_id=test-user", 40);
|
||
|
||
// CATEGORY 2: POST Endpoints (Write Performance)
|
||
console.log("\n📊 Category 2: POST Endpoints (Write Performance)");
|
||
console.log("=".repeat(70));
|
||
|
||
const createCreatorBody = {
|
||
user_id: `perf-test-${Date.now()}`,
|
||
username: `perf_user_${Math.random().toString(36).substring(7)}`,
|
||
bio: "Performance test creator",
|
||
experience_level: "junior",
|
||
};
|
||
|
||
console.log("\nCreate operations:");
|
||
await benchmarkEndpoint("POST", "/api/creators", 30, {
|
||
...createCreatorBody,
|
||
user_id: `perf-${Date.now()}-1`,
|
||
username: `perf_user_${Date.now()}_1`,
|
||
});
|
||
|
||
const createOppBody = {
|
||
user_id: `perf-creator-${Date.now()}`,
|
||
title: "Performance Test Job",
|
||
description: "Testing performance",
|
||
job_type: "contract",
|
||
};
|
||
|
||
await benchmarkEndpoint("POST", "/api/opportunities", 25, createOppBody);
|
||
await benchmarkEndpoint("POST", "/api/applications", 20, {
|
||
user_id: `perf-app-creator-${Date.now()}`,
|
||
opportunity_id: "test-opp-id",
|
||
cover_letter: "Performance test application",
|
||
});
|
||
|
||
// CATEGORY 3: PUT Endpoints (Update Performance)
|
||
console.log("\n📊 Category 3: PUT Endpoints (Update Performance)");
|
||
console.log("=".repeat(70));
|
||
|
||
console.log("\nUpdate operations:");
|
||
await benchmarkEndpoint("PUT", "/api/creators/test-id", 25, {
|
||
bio: "Updated bio",
|
||
skills: ["javascript", "typescript"],
|
||
});
|
||
|
||
await benchmarkEndpoint("PUT", "/api/opportunities/test-opp-id", 20, {
|
||
user_id: "test-user",
|
||
title: "Updated Title",
|
||
status: "closed",
|
||
});
|
||
|
||
// CATEGORY 4: Complex Queries
|
||
console.log("\n📊 Category 4: Complex & Heavy Queries");
|
||
console.log("=".repeat(70));
|
||
|
||
console.log("\nPaginated + Filtered queries:");
|
||
await benchmarkEndpoint(
|
||
"GET",
|
||
"/api/creators?arm=gameforge&search=test&page=1&limit=50",
|
||
30,
|
||
);
|
||
await benchmarkEndpoint(
|
||
"GET",
|
||
"/api/opportunities?arm=labs&experienceLevel=senior&jobType=full-time&page=1&limit=50",
|
||
25,
|
||
);
|
||
|
||
console.log("\nMulti-page traversal:");
|
||
await benchmarkEndpoint("GET", "/api/creators?page=2&limit=20", 30);
|
||
await benchmarkEndpoint("GET", "/api/creators?page=5&limit=20", 30);
|
||
await benchmarkEndpoint("GET", "/api/opportunities?page=3&limit=20", 25);
|
||
|
||
// Generate Report
|
||
console.log("\n" + "=".repeat(70));
|
||
console.log("\n📈 Performance Report\n");
|
||
|
||
// Group by endpoint
|
||
const grouped = new Map<string, PerformanceMetric[]>();
|
||
metrics.forEach((m) => {
|
||
const key = `${m.method} ${m.endpoint.split("?")[0]}`;
|
||
if (!grouped.has(key)) grouped.set(key, []);
|
||
grouped.get(key)!.push(m);
|
||
});
|
||
|
||
// Print detailed metrics
|
||
grouped.forEach((metricList, endpoint) => {
|
||
const avg =
|
||
metricList.reduce((sum, m) => sum + m.avgTime, 0) / metricList.length;
|
||
const p95 =
|
||
metricList.reduce((sum, m) => sum + m.p95Time, 0) / metricList.length;
|
||
const p99 =
|
||
metricList.reduce((sum, m) => sum + m.p99Time, 0) / metricList.length;
|
||
const rps =
|
||
metricList.reduce((sum, m) => sum + m.requestsPerSecond, 0) /
|
||
metricList.length;
|
||
|
||
console.log(`📍 ${endpoint}`);
|
||
console.log(` Avg: ${avg.toFixed(2)}ms`);
|
||
console.log(` P95: ${p95.toFixed(2)}ms`);
|
||
console.log(` P99: ${p99.toFixed(2)}ms`);
|
||
console.log(` RPS: ${rps.toFixed(2)}`);
|
||
console.log("");
|
||
});
|
||
|
||
// Performance targets
|
||
console.log("🎯 Performance Targets & Compliance\n");
|
||
|
||
const targets = {
|
||
"GET endpoints": { target: 100, actual: 0, threshold: "< 100ms" },
|
||
"POST endpoints": { target: 200, actual: 0, threshold: "< 200ms" },
|
||
"PUT endpoints": { target: 150, actual: 0, threshold: "< 150ms" },
|
||
"Complex queries": { target: 250, actual: 0, threshold: "< 250ms" },
|
||
};
|
||
|
||
let targetsPassed = 0;
|
||
let targetsFailed = 0;
|
||
|
||
metrics.forEach((m) => {
|
||
if (m.method === "GET") {
|
||
targets["GET endpoints"].actual = Math.max(
|
||
targets["GET endpoints"].actual,
|
||
m.avgTime,
|
||
);
|
||
} else if (m.method === "POST") {
|
||
targets["POST endpoints"].actual = Math.max(
|
||
targets["POST endpoints"].actual,
|
||
m.avgTime,
|
||
);
|
||
} else if (m.method === "PUT") {
|
||
targets["PUT endpoints"].actual = Math.max(
|
||
targets["PUT endpoints"].actual,
|
||
m.avgTime,
|
||
);
|
||
}
|
||
});
|
||
|
||
// Check complex queries
|
||
const complexQueries = metrics.filter(
|
||
(m) => m.endpoint.includes("?") && m.endpoint.split("&").length > 2,
|
||
);
|
||
if (complexQueries.length > 0) {
|
||
targets["Complex queries"].actual = Math.max(
|
||
...complexQueries.map((m) => m.avgTime),
|
||
);
|
||
}
|
||
|
||
Object.entries(targets).forEach(([category, data]) => {
|
||
if (data.actual === 0) return; // Skip if no data
|
||
|
||
const passed = data.actual <= data.target;
|
||
const symbol = passed ? "✓" : "✗";
|
||
console.log(
|
||
`${symbol} ${category.padEnd(20)} ${data.actual.toFixed(2)}ms ${data.threshold}`,
|
||
);
|
||
|
||
if (passed) targetsPassed++;
|
||
else targetsFailed++;
|
||
});
|
||
|
||
// Summary Statistics
|
||
console.log("\n" + "=".repeat(70));
|
||
console.log("\n📊 Summary Statistics\n");
|
||
|
||
const allTimes = metrics.map((m) => m.avgTime);
|
||
const slowestEndpoint = metrics.reduce((a, b) =>
|
||
a.avgTime > b.avgTime ? a : b,
|
||
);
|
||
const fastestEndpoint = metrics.reduce((a, b) =>
|
||
a.avgTime < b.avgTime ? a : b,
|
||
);
|
||
|
||
console.log(`Total Endpoints Tested: ${metrics.length}`);
|
||
console.log(
|
||
`Average Response Time: ${(allTimes.reduce((a, b) => a + b) / allTimes.length).toFixed(2)}ms`,
|
||
);
|
||
console.log(
|
||
`Fastest: ${fastestEndpoint.method} ${fastestEndpoint.endpoint.split("?")[0]} (${fastestEndpoint.avgTime.toFixed(2)}ms)`,
|
||
);
|
||
console.log(
|
||
`Slowest: ${slowestEndpoint.method} ${slowestEndpoint.endpoint.split("?")[0]} (${slowestEndpoint.avgTime.toFixed(2)}ms)`,
|
||
);
|
||
console.log(
|
||
`\nPerformance Targets: ${targetsPassed} passed, ${targetsFailed} failed`,
|
||
);
|
||
|
||
if (targetsFailed === 0) {
|
||
console.log("\n✅ All performance targets met!");
|
||
} else {
|
||
console.log(
|
||
`\n⚠️ ${targetsFailed} performance targets not met. Optimization needed.`,
|
||
);
|
||
}
|
||
|
||
console.log("\n" + "=".repeat(70));
|
||
return { passed: targetsPassed, failed: targetsFailed };
|
||
}
|
||
|
||
runPerformanceTests()
|
||
.then((summary) => {
|
||
process.exit(summary.failed > 0 ? 1 : 0);
|
||
})
|
||
.catch((error) => {
|
||
console.error("Performance test failed:", error);
|
||
process.exit(1);
|
||
});
|