diff --git a/.gitignore b/.gitignore index 0ff549a..682b318 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /bin/ -/obj/ \ No newline at end of file +/obj/ +/test/benchmark_output/ \ No newline at end of file diff --git a/StitcherApi.csproj b/StitcherApi.csproj index a437c5f..a2c1ab7 100644 --- a/StitcherApi.csproj +++ b/StitcherApi.csproj @@ -6,6 +6,6 @@ - + diff --git a/test/benchmark.js b/test/benchmark.js new file mode 100644 index 0000000..b56e61e --- /dev/null +++ b/test/benchmark.js @@ -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(); diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..c2b1bd2 --- /dev/null +++ b/test/package.json @@ -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" +} \ No newline at end of file