feat : add simple self-benchmark using node js with the help of LLM and upgrade SixLabors version
This commit is contained in:
parent
8cec6e92c5
commit
ff0a302e9f
4 changed files with 274 additions and 2 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
/bin/
|
/bin/
|
||||||
/obj/
|
/obj/
|
||||||
|
/test/benchmark_output/
|
||||||
|
|
@ -6,6 +6,6 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.5" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
259
test/benchmark.js
Normal file
259
test/benchmark.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const API_ENDPOINT = "http://localhost:5229/api/image/generate";
|
||||||
|
const NUM_REQUESTS_PER_SCENARIO = 10;
|
||||||
|
const OUTPUT_DIR = "benchmark_output";
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bright: "\x1b[1m",
|
||||||
|
dim: "\x1b[2m",
|
||||||
|
fg: {
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const scenarios = [
|
||||||
|
{
|
||||||
|
name: "Small Canvas, Small Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:B2",
|
||||||
|
crop_offset: [0.25, 0.25],
|
||||||
|
crop_size: [0.5, 0.5],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Medium Canvas, Full Crop (Stitching)",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "C3:F6",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Large Canvas, Small Corner Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:H12",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [0.1, 0.1],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Large Canvas, Center Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:AE55",
|
||||||
|
crop_offset: [0.45, 0.45],
|
||||||
|
crop_size: [0.1, 0.1],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tall Canvas, Full Crop & Scale",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:A20",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wide Canvas, Full Crop & Scale",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:T1",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single Tile, Full Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "K16:K16",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single Tile, Partial Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "K16:K16",
|
||||||
|
crop_offset: [0.1, 0.1],
|
||||||
|
crop_size: [0.25, 0.25],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Large Canvas, Bottom-Right Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:AE55",
|
||||||
|
crop_offset: [0.95, 0.95],
|
||||||
|
crop_size: [0.05, 0.05],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wide Canvas, Thin Horizontal Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:AE10",
|
||||||
|
crop_offset: [0.0, 0.5],
|
||||||
|
crop_size: [1.0, 0.01],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tall Canvas, Thin Vertical Crop",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:J55",
|
||||||
|
crop_offset: [0.5, 0.0],
|
||||||
|
crop_size: [0.01, 1.0],
|
||||||
|
output_scale: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Medium Canvas, Heavy Scaling",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "D4:G8",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 0.05,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MAXIMUM CANVAS (Streaming Test)",
|
||||||
|
payload: {
|
||||||
|
canvas_rect: "A1:AE55",
|
||||||
|
crop_offset: [0.0, 0.0],
|
||||||
|
crop_size: [1.0, 1.0],
|
||||||
|
output_scale: 0.01,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const calculateStats = (times) => {
|
||||||
|
if (times.length === 0) return null;
|
||||||
|
const sum = times.reduce((a, b) => a + b, 0);
|
||||||
|
const avg = sum / times.length;
|
||||||
|
const stdDev = Math.sqrt(
|
||||||
|
times.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) /
|
||||||
|
times.length
|
||||||
|
);
|
||||||
|
const sorted = [...times].sort((a, b) => a - b);
|
||||||
|
return {
|
||||||
|
avg: avg,
|
||||||
|
stdDev: stdDev,
|
||||||
|
median: sorted[Math.floor(sorted.length / 2)],
|
||||||
|
min: sorted[0],
|
||||||
|
max: sorted[sorted.length - 1],
|
||||||
|
throughput: 1000 / avg,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function runBenchmark() {
|
||||||
|
console.log(
|
||||||
|
`${colors.bright}${colors.fg.cyan}--- Starting Image Stitcher API Benchmark ---${colors.reset}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`${colors.fg.red}Error creating output directory: ${e.message}${colors.reset}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allResults = [];
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
console.log(
|
||||||
|
`\n${colors.fg.yellow}Running Scenario: '${scenario.name}' (${NUM_REQUESTS_PER_SCENARIO} requests)...${colors.reset}`
|
||||||
|
);
|
||||||
|
const responseTimes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < NUM_REQUESTS_PER_SCENARIO; i++) {
|
||||||
|
try {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const response = await fetch(API_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(scenario.payload),
|
||||||
|
});
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
responseTimes.push(endTime - startTime);
|
||||||
|
if (i === 0) {
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
const fileName = `${scenario.name
|
||||||
|
.replace(/[\s(),]/g, "_")
|
||||||
|
.toLowerCase()}.png`;
|
||||||
|
const filePath = path.join(OUTPUT_DIR, fileName);
|
||||||
|
await fs.writeFile(filePath, buffer);
|
||||||
|
console.log(
|
||||||
|
` ${colors.dim}Saved output image to '${filePath}'${colors.reset}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await response.arrayBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
` ${colors.fg.red}Request ${i + 1} failed with status ${
|
||||||
|
response.status
|
||||||
|
}${colors.reset}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
` ${colors.fg.red}Request ${i + 1} failed with an exception: ${
|
||||||
|
e.message
|
||||||
|
}${colors.reset}`
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = calculateStats(responseTimes);
|
||||||
|
if (stats) {
|
||||||
|
allResults.push({ name: scenario.name, stats });
|
||||||
|
console.log(
|
||||||
|
` ${colors.fg.green}Average Latency: ${stats.avg.toFixed(2)} ms${
|
||||||
|
colors.reset
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\n${colors.bright}${colors.fg.cyan}--- Benchmark Summary ---${colors.reset}`
|
||||||
|
);
|
||||||
|
for (const result of allResults) {
|
||||||
|
console.log(`\n${colors.bright}Scenario: ${result.name}${colors.reset}`);
|
||||||
|
const { stats } = result;
|
||||||
|
console.log(
|
||||||
|
` Avg Latency: ${stats.avg.toFixed(2)} ms (+/- ${stats.stdDev.toFixed(
|
||||||
|
2
|
||||||
|
)} ms)`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Details (ms): Median: ${stats.median.toFixed(
|
||||||
|
2
|
||||||
|
)} | Min: ${stats.min.toFixed(2)} | Max: ${stats.max.toFixed(2)}`
|
||||||
|
);
|
||||||
|
console.log(` Avg Throughput: ${stats.throughput.toFixed(2)} req/s`);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`\n${colors.bright}${colors.fg.cyan}--- Benchmark Complete ---${colors.reset}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
runBenchmark();
|
||||||
12
test/package.json
Normal file
12
test/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "api-benchmark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A benchmark script for the Image Stitcher API.",
|
||||||
|
"main": "benchmark.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node benchmark.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue