Files
CapaKraken/packages/staffing/src/__tests__/capacity-analyzer-perf.test.ts
T
Hartmut b9fd7fdb03 perf(staffing): replace day-by-day capacity loop with range arithmetic
Eliminates O(resource × allocation × days) iteration in findCapacityWindows
by pre-computing vacation date sets and using direct range overlap math.
Adds performance regression test (50 resources × 20 allocs × 365 days < 500ms).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:17:26 +02:00

98 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AllocationStatus } from "@capakraken/shared";
import { describe, expect, it } from "vitest";
import { findCapacityWindows, analyzeUtilization } from "../capacity-analyzer.js";
import type { CapacityAnalysisInput } from "../capacity-analyzer.js";
const standardAvailability = {
sunday: 0,
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
};
/**
* Performance regression test.
* 50 resources × 20 allocations × 365 days must complete in under 500 ms.
*/
describe("capacity-analyzer performance", () => {
it("analyzeUtilization: 50 resources × 20 allocations × 365 days < 500 ms", () => {
const analysisStart = new Date("2026-01-01");
const analysisEnd = new Date("2026-12-31");
const periodMs = analysisEnd.getTime() - analysisStart.getTime();
const resources: CapacityAnalysisInput[] = Array.from({ length: 50 }, (_, ri) => {
const allocations = Array.from({ length: 20 }, (_, ai) => {
// Distribute allocations pseudo-randomly across the year
const offset = Math.floor(((ri * 20 + ai) / 1000) * periodMs);
const start = new Date(analysisStart.getTime() + offset);
const end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000); // 30-day blocks
return {
startDate: start > analysisEnd ? analysisStart : start,
endDate: end > analysisEnd ? analysisEnd : end,
hoursPerDay: 4,
status: AllocationStatus.CONFIRMED,
projectName: `Project-${ri}-${ai}`,
isChargeable: ai % 2 === 0,
};
});
return {
resource: {
id: `res-${ri}`,
displayName: `Resource ${ri}`,
chargeabilityTarget: 80,
availability: standardAvailability,
},
allocations,
analysisStart,
analysisEnd,
};
});
const t0 = performance.now();
for (const input of resources) {
analyzeUtilization(input);
}
const elapsed = performance.now() - t0;
expect(elapsed).toBeLessThan(500);
});
it("findCapacityWindows: 50 resources × 20 allocations × 365 days < 500 ms", () => {
const searchStart = new Date("2026-01-01");
const searchEnd = new Date("2026-12-31");
const periodMs = searchEnd.getTime() - searchStart.getTime();
const simpleResource = {
id: "res-perf",
displayName: "Perf Resource",
availability: standardAvailability,
};
const t0 = performance.now();
for (let ri = 0; ri < 50; ri++) {
const allocations = Array.from({ length: 20 }, (_, ai) => {
const offset = Math.floor(((ri * 20 + ai) / 1000) * periodMs);
const start = new Date(searchStart.getTime() + offset);
const end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
return {
startDate: start > searchEnd ? searchStart : start,
endDate: end > searchEnd ? searchEnd : end,
hoursPerDay: 4,
status: AllocationStatus.CONFIRMED,
};
});
findCapacityWindows(simpleResource, allocations, searchStart, searchEnd);
}
const elapsed = performance.now() - t0;
expect(elapsed).toBeLessThan(500);
});
});