264 lines
8.6 KiB
TypeScript
264 lines
8.6 KiB
TypeScript
/**
|
|
* performance.test.ts — Performance tests for lock operations.
|
|
*
|
|
* Tests measure latency of:
|
|
* - Single lock acquisition (median, p95)
|
|
* - Single lock release
|
|
* - Conflict checking
|
|
* - Bulk acquire/release
|
|
* - Lock info retrieval
|
|
* - Registry operation cycle
|
|
*
|
|
* Thresholds define acceptable performance bounds.
|
|
*
|
|
* @module file-claiming/performance.test
|
|
*/
|
|
|
|
import { performance } from "node:perf_hooks";
|
|
import {
|
|
assert,
|
|
mockOwner,
|
|
TEST_FILE_A,
|
|
} from "./test-utils.ts";
|
|
|
|
// Performance thresholds (ms)
|
|
const THRESHOLDS = {
|
|
lockAcquisition: 10,
|
|
lockRelease: 5,
|
|
conflictCheck: 5,
|
|
bulkAcquire: 100,
|
|
bulkRelease: 50,
|
|
lockInfo: 5,
|
|
registryOperation: 10,
|
|
conflictResolution: 5,
|
|
cleanup: 20,
|
|
minThroughputPerMs: 0.01,
|
|
};
|
|
|
|
// Lazy module loading
|
|
let _acq: any = null;
|
|
let _reg: any = null;
|
|
let _cfg: any = null;
|
|
|
|
function getAcq() { if (!_acq) _acq = require("../src/lock-acquisition"); return _acq; }
|
|
function getReg() { if (!_reg) _reg = require("../index"); return _reg; }
|
|
function getCfg() { if (!_cfg) _cfg = require("../src/config"); return _cfg; }
|
|
|
|
function resetAll(): void {
|
|
getReg().resetRegistry();
|
|
getCfg().resetConfig();
|
|
}
|
|
|
|
function measureTimeSync<T>(fn: () => T): { result: T; elapsedMs: number } {
|
|
const start = performance.now();
|
|
const result = fn();
|
|
return { result, elapsedMs: performance.now() - start };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Single lock acquisition latency
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testSingleAcquisitionLatency() {
|
|
resetAll();
|
|
const { acquireLock } = getAcq();
|
|
const owner = mockOwner("agent", "perf-test");
|
|
|
|
const times: number[] = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const path = `/tmp/perf-lock-${i}.ts`;
|
|
const { elapsedMs } = measureTimeSync(() => {
|
|
acquireLock({ path, lockType: "write", owner, autoReleaseTTL: 300_000 });
|
|
});
|
|
times.push(elapsedMs);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
const sorted = [...times].sort((a, b) => a - b);
|
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
|
|
assert(avg < THRESHOLDS.lockAcquisition, `Avg ${avg.toFixed(3)}ms < ${THRESHOLDS.lockAcquisition}ms`);
|
|
console.log(` ✅ Single acquisition: avg=${avg.toFixed(3)}ms median=${median.toFixed(3)}ms p95=${p95.toFixed(3)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Single lock release latency
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testSingleReleaseLatency() {
|
|
resetAll();
|
|
const { acquireLock } = getAcq();
|
|
const registry = getReg().getClaimRegistry();
|
|
const owner = mockOwner("agent", "perf-release");
|
|
|
|
const claimIds: string[] = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const result = acquireLock({ path: `/tmp/perf-rel-${i}.ts`, lockType: "write", owner });
|
|
if (result.claim) claimIds.push(result.claim.id);
|
|
}
|
|
|
|
const times: number[] = [];
|
|
for (const claimId of claimIds) {
|
|
const { elapsedMs } = measureTimeSync(() => registry.release(claimId));
|
|
times.push(elapsedMs);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
assert(avg < THRESHOLDS.lockRelease, `Avg release ${avg.toFixed(3)}ms < ${THRESHOLDS.lockRelease}ms`);
|
|
console.log(` ✅ Single release: avg=${avg.toFixed(3)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Conflict check latency
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testConflictCheckLatency() {
|
|
resetAll();
|
|
const { acquireLock } = getAcq();
|
|
const registry = getReg().getClaimRegistry();
|
|
const owner = mockOwner("agent", "perf-c");
|
|
const other = mockOwner("agent", "perf-o");
|
|
|
|
acquireLock({ path: TEST_FILE_A, lockType: "write", owner });
|
|
|
|
const times: number[] = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const { elapsedMs } = measureTimeSync(() =>
|
|
registry.checkConflict(TEST_FILE_A, "write", other)
|
|
);
|
|
times.push(elapsedMs);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
assert(avg < THRESHOLDS.conflictCheck, `Avg conflict check ${avg.toFixed(3)}ms < ${THRESHOLDS.conflictCheck}ms`);
|
|
console.log(` ✅ Conflict check: avg=${avg.toFixed(3)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Bulk acquisition throughput
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testBulkAcquisition() {
|
|
resetAll();
|
|
const { acquireLock } = getAcq();
|
|
const owner = mockOwner("agent", "perf-bulk");
|
|
|
|
const count = 500;
|
|
const start = performance.now();
|
|
for (let i = 0; i < count; i++) {
|
|
acquireLock({ path: `/tmp/perf-bulk-${i}.ts`, lockType: "write", owner, autoReleaseTTL: 300_000 });
|
|
}
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert(elapsed < THRESHOLDS.bulkAcquire, `Bulk ${count} locks: ${elapsed.toFixed(0)}ms < ${THRESHOLDS.bulkAcquire}ms`);
|
|
console.log(` ✅ Bulk acquire ${count} locks: ${elapsed.toFixed(0)}ms (${(count/elapsed).toFixed(2)} ops/ms)`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Bulk release
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testBulkRelease() {
|
|
resetAll();
|
|
const { acquireLock } = getAcq();
|
|
const registry = getReg().getClaimRegistry();
|
|
const owner = mockOwner("agent", "perf-bulk-rel");
|
|
|
|
for (let i = 0; i < 500; i++) {
|
|
acquireLock({ path: `/tmp/perf-bulkr-${i}.ts`, lockType: "write", owner });
|
|
}
|
|
|
|
const start = performance.now();
|
|
registry.releaseAllByOwner(owner);
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert(elapsed < THRESHOLDS.bulkRelease, `Bulk release: ${elapsed.toFixed(0)}ms < ${THRESHOLDS.bulkRelease}ms`);
|
|
console.log(` ✅ Bulk release: ${elapsed.toFixed(0)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Lock info retrieval
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testLockInfoLatency() {
|
|
resetAll();
|
|
const { acquireLock, getLockInfo } = getAcq();
|
|
const owner = mockOwner("agent", "perf-info");
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
acquireLock({ path: `/tmp/perf-inf-${i}.ts`, lockType: i % 2 === 0 ? "write" : "read", owner });
|
|
}
|
|
|
|
const times: number[] = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
const { elapsedMs } = measureTimeSync(() => getLockInfo(`/tmp/perf-inf-${i}.ts`));
|
|
times.push(elapsedMs);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
assert(avg < THRESHOLDS.lockInfo, `Avg lock info ${avg.toFixed(3)}ms < ${THRESHOLDS.lockInfo}ms`);
|
|
console.log(` ✅ Lock info: avg=${avg.toFixed(3)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Registry operation cycle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function testRegistryOperationCycle() {
|
|
resetAll();
|
|
const registry = getReg().getClaimRegistry();
|
|
const owner = mockOwner("agent", "perf-cycle");
|
|
|
|
const times: number[] = [];
|
|
for (let i = 0; i < 500; i++) {
|
|
const claimId = `cycle-${i}`;
|
|
const path = `/tmp/perf-cyc-${i}.ts`;
|
|
const { elapsedMs } = measureTimeSync(() => {
|
|
registry.acquire({ id: claimId, path, lockType: "write", status: "active", owner, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
|
|
registry.checkConflict(path, "write", owner);
|
|
registry.release(claimId);
|
|
});
|
|
times.push(elapsedMs);
|
|
}
|
|
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
assert(avg < THRESHOLDS.registryOperation, `Avg cycle ${avg.toFixed(3)}ms < ${THRESHOLDS.registryOperation}ms`);
|
|
console.log(` ✅ Registry cycle: avg=${avg.toFixed(3)}ms`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test runner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function runTests() {
|
|
console.log("Running Performance Tests\n");
|
|
console.log("Thresholds:");
|
|
for (const [key, val] of Object.entries(THRESHOLDS)) {
|
|
console.log(` ${key}: ${val}ms`);
|
|
}
|
|
console.log("");
|
|
|
|
const tests = [
|
|
testSingleAcquisitionLatency,
|
|
testSingleReleaseLatency,
|
|
testConflictCheckLatency,
|
|
testBulkAcquisition,
|
|
testBulkRelease,
|
|
testLockInfoLatency,
|
|
testRegistryOperationCycle,
|
|
];
|
|
|
|
for (const test of tests) {
|
|
try {
|
|
test();
|
|
} catch (err) {
|
|
console.error(`\n❌ Performance test ${test.name} failed: ${err}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
console.log("\n✅ All performance tests passed!");
|
|
}
|
|
|
|
runTests();
|