717ae1031e
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { describe, test } from 'node:test';
|
|
import { CondensedGraph } from '../../src/core/CondensedGraph.js';
|
|
|
|
const runPerf = process.env.RUN_PERF_TESTS === '1';
|
|
const perfTest = runPerf ? test : test.skip;
|
|
|
|
describe('CondensedGraph - Realistic Authorization Workloads', () => {
|
|
perfTest('Zanzibar-style authorization graph', () => {
|
|
// Simulate a real authorization graph with:
|
|
// - Users, documents, groups, folders
|
|
// - Multiple relation types
|
|
// - Hierarchical access patterns
|
|
|
|
const graph = new CondensedGraph();
|
|
const numUsers = 1000;
|
|
const numDocs = 5000;
|
|
const numGroups = 50;
|
|
const numFolders = 100;
|
|
|
|
// Add users to groups (many-to-many)
|
|
for (let i = 0; i < numUsers; i++) {
|
|
const numGroupsPerUser = 1 + Math.floor(Math.random() * 5);
|
|
for (let j = 0; j < numGroupsPerUser; j++) {
|
|
const groupNum = Math.floor(Math.random() * numGroups);
|
|
graph.addEdge(`user:${i}`, 'member', `group:${groupNum}`);
|
|
}
|
|
}
|
|
|
|
// Add document ownership (one user per doc)
|
|
for (let i = 0; i < numDocs; i++) {
|
|
const ownerNum = Math.floor(Math.random() * numUsers);
|
|
graph.addEdge(`user:${ownerNum}`, 'owner', `doc:${i}`);
|
|
}
|
|
|
|
// Add documents to folders
|
|
for (let i = 0; i < numDocs; i++) {
|
|
const folderNum = Math.floor(Math.random() * numFolders);
|
|
graph.addEdge(`doc:${i}`, 'parent', `folder:${folderNum}`);
|
|
}
|
|
|
|
// Add folder ownership
|
|
for (let i = 0; i < numFolders; i++) {
|
|
const ownerNum = Math.floor(Math.random() * numUsers);
|
|
graph.addEdge(`user:${ownerNum}`, 'owner', `folder:${i}`);
|
|
}
|
|
|
|
// Add group access to documents
|
|
for (let i = 0; i < numDocs; i++) {
|
|
const numGroupsWithAccess = Math.floor(Math.random() * 3);
|
|
for (let j = 0; j < numGroupsWithAccess; j++) {
|
|
const groupNum = Math.floor(Math.random() * numGroups);
|
|
const relations = ['viewer', 'editor', 'commenter'];
|
|
const rel = relations[Math.floor(Math.random() * relations.length)];
|
|
graph.addEdge(`group:${groupNum}`, rel, `doc:${i}`);
|
|
}
|
|
}
|
|
|
|
graph.finalizePerfectHash();
|
|
graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });
|
|
|
|
const stats = graph.getStats();
|
|
|
|
console.log('\nZanzibar-style graph:');
|
|
console.log(` Users: ${numUsers}, Docs: ${numDocs}, Groups: ${numGroups}, Folders: ${numFolders}`);
|
|
console.log(` Total nodes: ${stats.numNodes}`);
|
|
console.log(` Total edges: ${stats.numEdges}`);
|
|
console.log(` Avg degree: ${stats.avgDegree.toFixed(2)}`);
|
|
console.log(` Memory: ${(stats.memoryUsage.total / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(` Bytes/edge: ${stats.bytesPerEdge.toFixed(2)}`);
|
|
|
|
// Authorization checks: user -> doc
|
|
console.log('\nAuthorization checks (user -> doc):');
|
|
|
|
const checks = [
|
|
{ user: 'user:0', doc: 'doc:0' },
|
|
{ user: 'user:100', doc: 'doc:500' },
|
|
{ user: 'user:500', doc: 'doc:1000' }
|
|
];
|
|
|
|
checks.forEach(check => {
|
|
const iterations = 100000;
|
|
|
|
// Direct ownership check
|
|
const directStart = performance.now();
|
|
for (let i = 0; i < iterations; i++) {
|
|
graph.findEdge(check.user, 'owner', check.doc);
|
|
}
|
|
const directTime = (performance.now() - directStart) / iterations * 1000;
|
|
|
|
// Has edge check
|
|
const hasStart = performance.now();
|
|
for (let i = 0; i < iterations; i++) {
|
|
graph.hasEdge(check.user, 'owner', check.doc);
|
|
}
|
|
const hasTime = (performance.now() - hasStart) / iterations * 1000;
|
|
|
|
console.log(` ${check.user} -> ${check.doc}:`);
|
|
console.log(` findEdge: ${directTime.toFixed(3)} µs`);
|
|
console.log(` hasEdge: ${hasTime.toFixed(3)} µs`);
|
|
});
|
|
|
|
// Group membership traversal simulation
|
|
console.log('\nGroup membership traversal:');
|
|
const userNum = 50;
|
|
const groups = graph.getOutEdgesByRel(`user:${userNum}`, 'member');
|
|
console.log(` User ${userNum} belongs to ${groups.length} groups`);
|
|
|
|
let totalDocsThroughGroups = 0;
|
|
let totalTime = 0;
|
|
const traversalIterations = 10000;
|
|
|
|
for (let i = 0; i < traversalIterations; i++) {
|
|
const start = performance.now();
|
|
let docCount = 0;
|
|
|
|
// Simulate: get user's groups, then get docs accessible by those groups
|
|
const userGroups = graph.getOutEdgesByRel(`user:${userNum}`, 'member');
|
|
for (const groupEdgeIdx of userGroups) {
|
|
const groupEdge = graph.getEdge(groupEdgeIdx);
|
|
const groupDocsViewer = graph.getOutEdgesByRel(groupEdge.dst, 'viewer');
|
|
const groupDocsEditor = graph.getOutEdgesByRel(groupEdge.dst, 'editor');
|
|
const groupDocsCommenter = graph.getOutEdgesByRel(groupEdge.dst, 'commenter');
|
|
docCount += groupDocsViewer.length + groupDocsEditor.length + groupDocsCommenter.length;
|
|
}
|
|
|
|
totalTime += performance.now() - start;
|
|
totalDocsThroughGroups += docCount;
|
|
}
|
|
|
|
const avgTime = (totalTime / traversalIterations) * 1000;
|
|
const avgDocs = totalDocsThroughGroups / traversalIterations;
|
|
|
|
console.log(` Avg time: ${avgTime.toFixed(3)} µs`);
|
|
console.log(` Avg docs accessible: ${avgDocs.toFixed(1)}`);
|
|
|
|
// Batch operations performance
|
|
console.log('\nBatch operations:');
|
|
|
|
// Batch getOutEdges
|
|
const batchStart = performance.now();
|
|
for (let i = 0; i < 1000; i++) {
|
|
const edges = graph.getOutEdgesByRel(`user:${i % numUsers}`, 'member');
|
|
}
|
|
const batchTime = performance.now() - batchStart;
|
|
console.log(` 1000 getOutEdges: ${batchTime.toFixed(2)} ms`);
|
|
|
|
assert.ok(stats.memoryUsage.total < 100 * 1024 * 1024, 'Memory should be < 100MB');
|
|
});
|
|
|
|
perfTest('high-frequency authorization checks', () => {
|
|
const graph = new CondensedGraph();
|
|
|
|
// Build a realistic graph
|
|
for (let i = 0; i < 10000; i++) {
|
|
graph.addEdge(`user:${i % 100}`, ['owner', 'editor', 'viewer'][i % 3], `doc:${i % 1000}`);
|
|
}
|
|
|
|
graph.finalizePerfectHash();
|
|
graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });
|
|
|
|
const numChecks = 1000000;
|
|
const users = Array.from({ length: 100 }, (_, i) => `user:${i}`);
|
|
const docs = Array.from({ length: 1000 }, (_, i) => `doc:${i}`);
|
|
for (let i = 0; i < 10000; i++) {
|
|
const user = users[i % 100];
|
|
const doc = docs[i % 1000];
|
|
graph.findEdge(user, 'owner', doc);
|
|
graph.findEdge(user, 'editor', doc);
|
|
graph.findEdge(user, 'viewer', doc);
|
|
}
|
|
|
|
const start = performance.now();
|
|
let allowed = 0;
|
|
|
|
for (let i = 0; i < numChecks; i++) {
|
|
const user = users[i % 100];
|
|
const doc = docs[i % 1000];
|
|
const hasOwner = graph.findEdge(user, 'owner', doc);
|
|
const hasEditor = graph.findEdge(user, 'editor', doc);
|
|
const hasViewer = graph.findEdge(user, 'viewer', doc);
|
|
|
|
if (hasOwner !== null || hasEditor !== null || hasViewer !== null) {
|
|
allowed++;
|
|
}
|
|
}
|
|
|
|
const duration = performance.now() - start;
|
|
const avgTime = (duration / numChecks) * 1000;
|
|
const opsPerSec = numChecks / (duration / 1000);
|
|
|
|
console.log('\nHigh-frequency authorization checks (1M checks):');
|
|
console.log(` Total time: ${duration.toFixed(2)} ms`);
|
|
console.log(` Avg time/check: ${avgTime.toFixed(3)} µs`);
|
|
console.log(` Checks/sec: ${opsPerSec.toFixed(0)}`);
|
|
console.log(` Allowed: ${allowed} (${(allowed / numChecks * 100).toFixed(1)}%)`);
|
|
|
|
assert.ok(avgTime < 80, `Should be fast, got ${avgTime.toFixed(3)} µs`);
|
|
});
|
|
|
|
perfTest('reachability query simulation', () => {
|
|
// Simulate hierarchical access: user -> group -> subgroups -> docs
|
|
const graph = new CondensedGraph();
|
|
|
|
const numUsers = 100;
|
|
const numGroups = 50;
|
|
const numDocs = 1000;
|
|
|
|
// User -> group membership
|
|
for (let i = 0; i < numUsers; i++) {
|
|
for (let j = 0; j < 3; j++) {
|
|
const groupNum = (i + j) % numGroups;
|
|
graph.addEdge(`user:${i}`, 'member', `group:${groupNum}`);
|
|
}
|
|
}
|
|
|
|
// Group hierarchy
|
|
for (let i = 0; i < numGroups; i++) {
|
|
if (i < numGroups - 1) {
|
|
graph.addEdge(`group:${i}`, 'parent', `group:${i + 1}`);
|
|
}
|
|
}
|
|
|
|
// Group -> doc access
|
|
for (let i = 0; i < numGroups; i++) {
|
|
for (let j = 0; j < 20; j++) {
|
|
graph.addEdge(`group:${i}`, 'viewer', `doc:${(i * 20 + j) % numDocs}`);
|
|
}
|
|
}
|
|
|
|
graph.finalizePerfectHash();
|
|
graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });
|
|
|
|
const stats = graph.getStats();
|
|
console.log('\nReachability graph:');
|
|
console.log(` Nodes: ${stats.numNodes}, Edges: ${stats.numEdges}`);
|
|
console.log(` Memory: ${(stats.memoryUsage.total / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
// Simulate BFS-like queries
|
|
const queries = 10000;
|
|
const start = performance.now();
|
|
|
|
for (let i = 0; i < queries; i++) {
|
|
const user = `user:${i % numUsers}`;
|
|
|
|
// Get user's groups
|
|
const userGroups = graph.getOutEdgesByRel(user, 'member');
|
|
|
|
// For each group, get parent groups
|
|
let allGroups = new Set();
|
|
for (const edgeIdx of userGroups) {
|
|
const edge = graph.getEdge(edgeIdx);
|
|
allGroups.add(edge.dst);
|
|
|
|
// Check for parent groups
|
|
const parentEdge = graph.findEdge(edge.dst, 'parent');
|
|
if (parentEdge !== null) {
|
|
const parent = graph.getEdge(parentEdge);
|
|
allGroups.add(parent.dst);
|
|
}
|
|
}
|
|
|
|
// Get docs accessible by all groups
|
|
let docCount = 0;
|
|
for (const group of allGroups) {
|
|
const docEdges = graph.getOutEdgesByRel(group, 'viewer');
|
|
docCount += docEdges.length;
|
|
}
|
|
}
|
|
|
|
const duration = performance.now() - start;
|
|
const avgTime = (duration / queries) * 1000;
|
|
|
|
console.log(`\nReachability queries (${queries} queries):`);
|
|
console.log(` Total time: ${duration.toFixed(2)} ms`);
|
|
console.log(` Avg time/query: ${avgTime.toFixed(3)} µs`);
|
|
console.log(` Queries/sec: ${(queries / (duration / 1000)).toFixed(0)}`);
|
|
|
|
assert.ok(avgTime < 100, `Should be fast, got ${avgTime.toFixed(3)} µs`);
|
|
});
|
|
|
|
perfTest('memory scalability comparison', () => {
|
|
const sizes = [10000, 50000, 100000];
|
|
|
|
console.log('\nMemory scalability:');
|
|
console.log('Edges | Memory (MB) | Bytes/Edge | Capacity | Utilization');
|
|
console.log('-------|-------------|------------|----------|-------------');
|
|
|
|
sizes.forEach(size => {
|
|
const graph = new CondensedGraph();
|
|
|
|
for (let i = 0; i < size; i++) {
|
|
graph.addEdge(`user:${i % 100}`, ['owner', 'editor', 'viewer'][i % 3], `doc:${i % 1000}`);
|
|
}
|
|
|
|
graph.finalizePerfectHash();
|
|
graph.finalizeWaveletAdjacency({ dropAdjacencyList: true });
|
|
|
|
const stats = graph.getStats();
|
|
const memMB = stats.memoryUsage.total / 1024 / 1024;
|
|
|
|
console.log(
|
|
`${size.toString().padEnd(7)} | ` +
|
|
`${memMB.toFixed(2).padEnd(11)} | ` +
|
|
`${stats.bytesPerEdge.toFixed(2).padEnd(10)} | ` +
|
|
`${stats.capacity.toString().padEnd(8)} | ` +
|
|
`${(stats.utilization * 100).toFixed(1).padEnd(10)}%`
|
|
);
|
|
|
|
assert.ok(memMB < size / 100, `Memory should be reasonable for ${size} edges`);
|
|
});
|
|
});
|
|
});
|