b9fd7fdb03
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>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
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);
|
||
});
|
||
});
|