946 lines
35 KiB
JavaScript
946 lines
35 KiB
JavaScript
|
|
import { performance } from 'perf_hooks';
|
||
|
|
import { Arbiter } from '../src/index.js';
|
||
|
|
|
||
|
|
console.log('⛓️ ChainRule Performance Benchmark Analysis\n');
|
||
|
|
|
||
|
|
// Benchmark utilities
|
||
|
|
class ChainBenchmark {
|
||
|
|
constructor(name) {
|
||
|
|
this.name = name;
|
||
|
|
this.results = [];
|
||
|
|
this.labels = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
async run(fn, iterations = 1000, label = '') {
|
||
|
|
// Warmup
|
||
|
|
for (let i = 0; i < Math.min(10, iterations / 10); i++) {
|
||
|
|
try {
|
||
|
|
await fn();
|
||
|
|
} catch (e) {
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Actual benchmark
|
||
|
|
const times = [];
|
||
|
|
const totalStart = performance.now();
|
||
|
|
for (let i = 0; i < iterations; i++) {
|
||
|
|
try {
|
||
|
|
const start = performance.now();
|
||
|
|
await fn();
|
||
|
|
const end = performance.now();
|
||
|
|
times.push(end - start);
|
||
|
|
} catch (e) {
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const totalTime = performance.now() - totalStart;
|
||
|
|
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
||
|
|
const min = Math.min(...times);
|
||
|
|
const max = Math.max(...times);
|
||
|
|
const p50 = times.sort((a, b) => a - b)[Math.floor(times.length * 0.5)];
|
||
|
|
const p95 = times[Math.floor(times.length * 0.95)];
|
||
|
|
const p99 = times[Math.floor(times.length * 0.99)];
|
||
|
|
const qps = Math.round(iterations / (totalTime / 1000));
|
||
|
|
this.results.push({
|
||
|
|
avg: avg.toFixed(3),
|
||
|
|
min: min.toFixed(3),
|
||
|
|
max: max.toFixed(3),
|
||
|
|
p50: p50.toFixed(3),
|
||
|
|
p95: p95.toFixed(3),
|
||
|
|
p99: p99.toFixed(3),
|
||
|
|
qps: qps
|
||
|
|
});
|
||
|
|
this.labels.push(label);
|
||
|
|
return { avg, min, max, p50, p95, p99, qps };
|
||
|
|
}
|
||
|
|
|
||
|
|
report() {
|
||
|
|
console.log('Configuration | Avg (ms) | P50 (ms) | P95 (ms) | P99 (ms) | QPS | Speedup');
|
||
|
|
console.log('---------------------------------|----------|----------|----------|----------|---------|--------');
|
||
|
|
const baselineQPS = this.results[0].qps;
|
||
|
|
this.results.forEach((result, i) => {
|
||
|
|
const label = this.labels[i] || `Test ${i + 1}`;
|
||
|
|
const speedup = i === 0 ? '1.00x' : `${(result.qps / baselineQPS).toFixed(2)}x`;
|
||
|
|
console.log(`${label.padEnd(32)} | ${result.avg.padStart(8)} | ${result.p50.padStart(8)} | ${result.p95.padStart(8)} | ${result.p99.padStart(8)} | ${result.qps.toString().padStart(7)} | ${speedup.padStart(6)}`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Enterprise organizational data generator
|
||
|
|
function setupEnterpriseChainGraph(scale = 'medium') {
|
||
|
|
const scales = {
|
||
|
|
small: { users: 200, groups: 50, projects: 20, departments: 8, budgets: 100 },
|
||
|
|
medium: { users: 2000, groups: 200, projects: 100, departments: 15, budgets: 500 },
|
||
|
|
large: { users: 10000, groups: 1000, projects: 500, departments: 25, budgets: 2000 },
|
||
|
|
xlarge: { users: 20000, groups: 5000, projects: 2000, departments: 40, budgets: 8000 },
|
||
|
|
xxlarge: { users: 100000, groups: 10000, projects: 5000, departments: 50, budgets: 20000 }
|
||
|
|
};
|
||
|
|
|
||
|
|
const config = scales[scale];
|
||
|
|
console.log(`🏢 Setting up ${scale} enterprise graph: ${config.users} users, ${config.groups} groups, ${config.projects} projects`);
|
||
|
|
|
||
|
|
const arbiter = new Arbiter({
|
||
|
|
fastConstructionMode: true,
|
||
|
|
enableInference: false // Disable by default for clean baseline measurements
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create organizational hierarchy: users → groups → departments → divisions → company
|
||
|
|
const entities = {
|
||
|
|
users: [],
|
||
|
|
groups: [],
|
||
|
|
departments: [],
|
||
|
|
divisions: ['engineering', 'sales', 'marketing', 'finance', 'operations'],
|
||
|
|
projects: [],
|
||
|
|
budgets: [],
|
||
|
|
resources: [],
|
||
|
|
facilities: []
|
||
|
|
};
|
||
|
|
|
||
|
|
// Generate users
|
||
|
|
for (let i = 0; i < config.users; i++) {
|
||
|
|
const userKey = `user:emp${i}`;
|
||
|
|
entities.users.push(userKey);
|
||
|
|
arbiter.addNode(userKey, 'user');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate groups (teams within departments)
|
||
|
|
for (let i = 0; i < config.groups; i++) {
|
||
|
|
const groupKey = `group:team${i}`;
|
||
|
|
entities.groups.push(groupKey);
|
||
|
|
arbiter.addNode(groupKey, 'group');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate departments
|
||
|
|
for (let i = 0; i < config.departments; i++) {
|
||
|
|
const deptKey = `dept:dept${i}`;
|
||
|
|
entities.departments.push(deptKey);
|
||
|
|
arbiter.addNode(deptKey, 'department');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate divisions
|
||
|
|
entities.divisions.forEach(div => {
|
||
|
|
const divKey = `division:${div}`;
|
||
|
|
arbiter.addNode(divKey, 'division');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add company root
|
||
|
|
arbiter.addNode('company:acme', 'company');
|
||
|
|
|
||
|
|
// Generate projects
|
||
|
|
for (let i = 0; i < config.projects; i++) {
|
||
|
|
const projectKey = `project:proj${i}`;
|
||
|
|
entities.projects.push(projectKey);
|
||
|
|
arbiter.addNode(projectKey, 'project');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate budgets with values
|
||
|
|
for (let i = 0; i < config.budgets; i++) {
|
||
|
|
const budgetKey = `budget:budget${i}`;
|
||
|
|
const value = Math.floor(Math.random() * 5000000) + 100000; // $100K to $5M
|
||
|
|
entities.budgets.push({ key: budgetKey, value });
|
||
|
|
arbiter.addNode(budgetKey, 'budget');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate resources (servers, databases, etc.)
|
||
|
|
for (let i = 0; i < config.projects / 2; i++) {
|
||
|
|
const resourceKey = `resource:res${i}`;
|
||
|
|
const cost = Math.floor(Math.random() * 100000) + 5000; // $5K to $100K
|
||
|
|
entities.resources.push({ key: resourceKey, cost });
|
||
|
|
arbiter.addNode(resourceKey, 'resource');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate facilities
|
||
|
|
const facilityNames = ['hq', 'east-office', 'west-office', 'remote', 'datacenter'];
|
||
|
|
facilityNames.forEach(name => {
|
||
|
|
const facilityKey = `facility:${name}`;
|
||
|
|
entities.facilities.push(facilityKey);
|
||
|
|
arbiter.addNode(facilityKey, 'facility');
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(' 🔗 Building organizational chains...');
|
||
|
|
|
||
|
|
// Build 5-level organizational hierarchy: user → group → department → division → company
|
||
|
|
|
||
|
|
// User → Group membership
|
||
|
|
entities.users.forEach(user => {
|
||
|
|
// Each user belongs to 1-3 groups
|
||
|
|
const groupCount = Math.floor(Math.random() * 3) + 1;
|
||
|
|
for (let i = 0; i < groupCount; i++) {
|
||
|
|
const group = entities.groups[Math.floor(Math.random() * entities.groups.length)];
|
||
|
|
arbiter.addRelation(user, 'member_of', group);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Group → Department membership
|
||
|
|
entities.groups.forEach(group => {
|
||
|
|
const dept = entities.departments[Math.floor(Math.random() * entities.departments.length)];
|
||
|
|
arbiter.addRelation(group, 'belongs_to', dept);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Department → Division membership
|
||
|
|
entities.departments.forEach(dept => {
|
||
|
|
const division = entities.divisions[Math.floor(Math.random() * entities.divisions.length)];
|
||
|
|
arbiter.addRelation(dept, 'part_of', `division:${division}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Division → Company membership
|
||
|
|
entities.divisions.forEach(div => {
|
||
|
|
arbiter.addRelation(`division:${div}`, 'part_of', 'company:acme');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Project chains: user → group → project → budget
|
||
|
|
entities.projects.forEach(project => {
|
||
|
|
// Projects owned by groups
|
||
|
|
const ownerGroup = entities.groups[Math.floor(Math.random() * entities.groups.length)];
|
||
|
|
arbiter.addRelation(ownerGroup, 'manages', project);
|
||
|
|
|
||
|
|
// Projects have budgets
|
||
|
|
const budget = entities.budgets[Math.floor(Math.random() * entities.budgets.length)];
|
||
|
|
arbiter.addRelation(project, 'has_budget', budget.key, { value: budget.value });
|
||
|
|
|
||
|
|
// Projects have resource costs
|
||
|
|
if (Math.random() < 0.7) {
|
||
|
|
const resource = entities.resources[Math.floor(Math.random() * entities.resources.length)];
|
||
|
|
arbiter.addRelation(project, 'uses_resource', resource.key, { cost: resource.cost });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Facility chains: user → facility, department → facility
|
||
|
|
entities.users.forEach(user => {
|
||
|
|
const facility = entities.facilities[Math.floor(Math.random() * entities.facilities.length)];
|
||
|
|
arbiter.addRelation(user, 'located_in', facility);
|
||
|
|
});
|
||
|
|
|
||
|
|
entities.departments.forEach(dept => {
|
||
|
|
const facility = entities.facilities[Math.floor(Math.random() * entities.facilities.length)];
|
||
|
|
arbiter.addRelation(dept, 'operates_in', facility);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Configure basic relation types
|
||
|
|
const relationTypes = [
|
||
|
|
'member_of', 'belongs_to', 'part_of', 'manages', 'has_budget',
|
||
|
|
'uses_resource', 'located_in', 'operates_in', 'can_access', 'has_permission'
|
||
|
|
];
|
||
|
|
|
||
|
|
relationTypes.forEach(rel => {
|
||
|
|
arbiter.setRelationConfig(rel, { type: 'direct' });
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(' ✅ Enterprise chain graph ready');
|
||
|
|
|
||
|
|
// Initialize PLTC for reachability checks
|
||
|
|
console.log(' ⚡ Initializing PLTC indices...');
|
||
|
|
const initStart = Date.now();
|
||
|
|
arbiter.graphManager.initializeReachabilityChecker();
|
||
|
|
const initTime = Date.now() - initStart;
|
||
|
|
console.log(` ✅ PLTC initialized in ${initTime}ms`);
|
||
|
|
|
||
|
|
return { arbiter, ...entities };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Benchmark 1: Chain Length Performance
|
||
|
|
async function benchmarkChainLength() {
|
||
|
|
console.log('📏 Benchmark 1: Chain Length Performance Impact\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('medium');
|
||
|
|
const benchmark = new ChainBenchmark('Chain Length Performance');
|
||
|
|
|
||
|
|
// Baseline: Direct access (no chain)
|
||
|
|
graph.arbiter.setRelationConfig('direct_access', {
|
||
|
|
type: 'direct',
|
||
|
|
relation: 'can_access'
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create a substantial number of direct access relations for realistic testing
|
||
|
|
// Instead of 100 random relations, create relations for every user to some projects
|
||
|
|
const directAccessPairs = [];
|
||
|
|
graph.users.forEach((user, i) => {
|
||
|
|
// Each user has direct access to 1-3 projects
|
||
|
|
const numProjects = Math.floor(Math.random() * 3) + 1;
|
||
|
|
for (let j = 0; j < numProjects; j++) {
|
||
|
|
const project = graph.projects[(i * 7 + j) % graph.projects.length]; // Deterministic but spread out
|
||
|
|
graph.arbiter.addRelation(user, 'can_access', project);
|
||
|
|
directAccessPairs.push({ user, project });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(` ✅ Created ${directAccessPairs.length} direct access relations (${(directAccessPairs.length / (graph.users.length * graph.projects.length) * 100).toFixed(1)}% coverage)`);
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
// Test EXISTING relations for accurate performance measurement
|
||
|
|
const pair = directAccessPairs[Math.floor(Math.random() * directAccessPairs.length)];
|
||
|
|
return graph.arbiter.check(pair.user, 'direct_access', pair.project, { noInfer: true });
|
||
|
|
}, 1000, 'Direct Access (baseline)');
|
||
|
|
|
||
|
|
// 2-step chain: user → group → project
|
||
|
|
graph.arbiter.setRelationConfig('chain_2_step', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
// Build collection of valid 2-step chain paths for testing
|
||
|
|
const validChain2Paths = [];
|
||
|
|
graph.users.forEach(user => {
|
||
|
|
// Find groups this user belongs to
|
||
|
|
const userGroups = graph.arbiter.relationManager.getRelationsFromSrc(user, 'member_of');
|
||
|
|
userGroups.forEach(groupRel => {
|
||
|
|
// Find projects this group manages
|
||
|
|
const groupProjects = graph.arbiter.relationManager.getRelationsFromSrc(groupRel.object, 'manages');
|
||
|
|
groupProjects.forEach(projectRel => {
|
||
|
|
validChain2Paths.push({ user, project: projectRel.object });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(` ✅ Found ${validChain2Paths.length} valid 2-step chain paths (user→group→project)`);
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
// Test EXISTING chain paths for accurate performance measurement
|
||
|
|
if (validChain2Paths.length === 0) {
|
||
|
|
// Fallback to random if no valid paths found
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_2_step', project, { noInfer: true });
|
||
|
|
} else {
|
||
|
|
const path = validChain2Paths[Math.floor(Math.random() * validChain2Paths.length)];
|
||
|
|
return graph.arbiter.check(path.user, 'chain_2_step', path.project, { noInfer: true });
|
||
|
|
}
|
||
|
|
}, 1000, '2-Step Chain (user→group→project)');
|
||
|
|
|
||
|
|
// 3-step chain: user → group → department → division
|
||
|
|
graph.arbiter.setRelationConfig('chain_3_step', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'belongs_to', direction: 'out' },
|
||
|
|
{ relation: 'part_of', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
// Build collection of valid 3-step chain paths for testing
|
||
|
|
const validChain3Paths = [];
|
||
|
|
graph.users.forEach(user => {
|
||
|
|
// Find groups this user belongs to
|
||
|
|
const userGroups = graph.arbiter.relationManager.getRelationsFromSrc(user, 'member_of');
|
||
|
|
userGroups.forEach(groupRel => {
|
||
|
|
// Find departments this group belongs to
|
||
|
|
const groupDepts = graph.arbiter.relationManager.getRelationsFromSrc(groupRel.object, 'belongs_to');
|
||
|
|
groupDepts.forEach(deptRel => {
|
||
|
|
// Find divisions this department is part of
|
||
|
|
const deptDivisions = graph.arbiter.relationManager.getRelationsFromSrc(deptRel.object, 'part_of');
|
||
|
|
deptDivisions.forEach(divisionRel => {
|
||
|
|
validChain3Paths.push({ user, division: divisionRel.object });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(` ✅ Found ${validChain3Paths.length} valid 3-step chain paths (user→group→dept→division)`);
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
// Test EXISTING chain paths for accurate performance measurement
|
||
|
|
if (validChain3Paths.length === 0) {
|
||
|
|
// Fallback to random if no valid paths found
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_3_step', `division:${division}`, { noInfer: true });
|
||
|
|
} else {
|
||
|
|
const path = validChain3Paths[Math.floor(Math.random() * validChain3Paths.length)];
|
||
|
|
return graph.arbiter.check(path.user, 'chain_3_step', path.division, { noInfer: true });
|
||
|
|
}
|
||
|
|
}, 1000, '3-Step Chain (user→group→dept→division)');
|
||
|
|
|
||
|
|
// 4-step chain: user → group → project → budget → facility (create a valid 4-step path)
|
||
|
|
// First add budget→facility relationships to create valid 4-step chains
|
||
|
|
graph.budgets.forEach(budget => {
|
||
|
|
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||
|
|
graph.arbiter.addRelation(budget.key, 'allocated_to', facility);
|
||
|
|
});
|
||
|
|
|
||
|
|
graph.arbiter.setRelationConfig('allocated_to', { type: 'direct' });
|
||
|
|
graph.arbiter.setRelationConfig('chain_4_step', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' }, // user → group
|
||
|
|
{ relation: 'manages', direction: 'out' }, // group → project
|
||
|
|
{ relation: 'has_budget', direction: 'out' }, // project → budget
|
||
|
|
{ relation: 'allocated_to', direction: 'out' } // budget → facility
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_4_step', facility, { noInfer: true });
|
||
|
|
}, 1000, '4-Step Chain (user→group→project→budget→facility)');
|
||
|
|
|
||
|
|
// 5-step chain: user → group → department → division → company → facility
|
||
|
|
// Add company→facility relationship for valid 5-step chain
|
||
|
|
graph.facilities.forEach(facility => {
|
||
|
|
graph.arbiter.addRelation('company:acme', 'operates', facility);
|
||
|
|
});
|
||
|
|
|
||
|
|
graph.arbiter.setRelationConfig('operates', { type: 'direct' });
|
||
|
|
graph.arbiter.setRelationConfig('chain_5_step', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' }, // user → group
|
||
|
|
{ relation: 'belongs_to', direction: 'out' }, // group → department
|
||
|
|
{ relation: 'part_of', direction: 'out' }, // department → division
|
||
|
|
{ relation: 'part_of', direction: 'out' }, // division → company
|
||
|
|
{ relation: 'operates', direction: 'out' } // company → facility
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_5_step', facility, { noInfer: true });
|
||
|
|
}, 1000, '5-Step Chain (user→group→dept→div→company→facility)');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Benchmark 2: ChainRule vs Traditional Approaches
|
||
|
|
async function benchmarkVsTraditional() {
|
||
|
|
console.log('⚔️ Benchmark 2: ChainRule vs Traditional Authorization\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('medium');
|
||
|
|
const benchmark = new ChainBenchmark('ChainRule vs Traditional');
|
||
|
|
|
||
|
|
// Traditional ParentRule approach
|
||
|
|
graph.arbiter.setRelationConfig('traditional_parent', {
|
||
|
|
type: 'parent',
|
||
|
|
parentRelation: 'manages',
|
||
|
|
relation: 'member_of'
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'traditional_parent', project, { noInfer: true });
|
||
|
|
}, 1000, 'ParentRule (baseline)');
|
||
|
|
|
||
|
|
// Equivalent ChainRule approach
|
||
|
|
graph.arbiter.setRelationConfig('chain_equivalent', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_equivalent', project, { noInfer: true });
|
||
|
|
}, 1000, 'ChainRule (equivalent logic)');
|
||
|
|
|
||
|
|
// MultiHopRule approach
|
||
|
|
graph.arbiter.setRelationConfig('multihop_approach', {
|
||
|
|
type: 'multi_hop',
|
||
|
|
relation: 'member_of',
|
||
|
|
maxDepth: 3,
|
||
|
|
pathAggregation: 'max'
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'multihop_approach', project, { noInfer: true });
|
||
|
|
}, 1000, 'MultiHopRule (flexible paths)');
|
||
|
|
|
||
|
|
// ChainRule with reverse direction
|
||
|
|
graph.arbiter.setRelationConfig('chain_reverse', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'manages', direction: 'in' },
|
||
|
|
{ relation: 'member_of', direction: 'in' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
return graph.arbiter.check(project, 'chain_reverse', user, { noInfer: true });
|
||
|
|
}, 1000, 'ChainRule (reverse direction)');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Benchmark 3: Value Extraction Performance
|
||
|
|
async function benchmarkValueExtraction() {
|
||
|
|
console.log('💰 Benchmark 3: Value Extraction Performance\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('medium');
|
||
|
|
const benchmark = new ChainBenchmark('Value Extraction Performance');
|
||
|
|
|
||
|
|
// Chain without value extraction (baseline)
|
||
|
|
graph.arbiter.setRelationConfig('chain_no_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_no_values', budget.key);
|
||
|
|
}, 1000, 'Chain (no value extraction)');
|
||
|
|
|
||
|
|
// Chain with value extraction - SUM aggregation
|
||
|
|
graph.arbiter.setRelationConfig('chain_sum_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_sum_values', budget.key);
|
||
|
|
}, 1000, 'Chain with SUM aggregation');
|
||
|
|
|
||
|
|
// Chain with value extraction - MAX aggregation
|
||
|
|
graph.arbiter.setRelationConfig('chain_max_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'max'
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_max_values', budget.key);
|
||
|
|
}, 1000, 'Chain with MAX aggregation');
|
||
|
|
|
||
|
|
// Chain with value extraction - MIN aggregation
|
||
|
|
graph.arbiter.setRelationConfig('chain_min_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'min'
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_min_values', budget.key);
|
||
|
|
}, 1000, 'Chain with MIN aggregation');
|
||
|
|
|
||
|
|
// Chain with OWA fusion aggregation
|
||
|
|
graph.arbiter.setRelationConfig('chain_owa_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'optimistic',
|
||
|
|
owaWeights: [0.7, 0.5, 0.3, 0.1]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_owa_values', budget.key);
|
||
|
|
}, 1000, 'Chain with OWA aggregation');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Benchmark 4: Direction and Pattern Performance
|
||
|
|
async function benchmarkDirectionPatterns() {
|
||
|
|
console.log('🧭 Benchmark 4: Chain Direction and Pattern Performance\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('medium');
|
||
|
|
const benchmark = new ChainBenchmark('Direction and Pattern Performance');
|
||
|
|
|
||
|
|
// Forward chain (baseline)
|
||
|
|
graph.arbiter.setRelationConfig('forward_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'belongs_to', direction: 'out' },
|
||
|
|
{ relation: 'part_of', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||
|
|
return graph.arbiter.check(user, 'forward_chain', `division:${division}`);
|
||
|
|
}, 1000, 'Forward Chain (baseline)');
|
||
|
|
|
||
|
|
// Backward chain
|
||
|
|
graph.arbiter.setRelationConfig('backward_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'part_of', direction: 'in' },
|
||
|
|
{ relation: 'belongs_to', direction: 'in' },
|
||
|
|
{ relation: 'member_of', direction: 'in' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
return graph.arbiter.check(`division:${division}`, 'backward_chain', user);
|
||
|
|
}, 1000, 'Backward Chain');
|
||
|
|
|
||
|
|
// Mixed direction chain
|
||
|
|
graph.arbiter.setRelationConfig('mixed_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' }, // user → group
|
||
|
|
{ relation: 'belongs_to', direction: 'in' }, // group ← department
|
||
|
|
{ relation: 'operates_in', direction: 'out' } // department → facility
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||
|
|
return graph.arbiter.check(user, 'mixed_chain', facility);
|
||
|
|
}, 1000, 'Mixed Direction Chain');
|
||
|
|
|
||
|
|
// Parallel chains pattern (testing multiple chain endpoints)
|
||
|
|
graph.arbiter.setRelationConfig('parallel_chains', {
|
||
|
|
union: [
|
||
|
|
{
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'located_in', direction: 'out' },
|
||
|
|
{ relation: 'operates_in', direction: 'in' },
|
||
|
|
{ relation: 'belongs_to', direction: 'in' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'parallel_chains', project);
|
||
|
|
}, 1000, 'Parallel Chains (Union)');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Benchmark 5: ChainRule with Inference
|
||
|
|
async function benchmarkChainInference() {
|
||
|
|
console.log('🧠 Benchmark 5: ChainRule with Inference Integration\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('small'); // Smaller graph for inference
|
||
|
|
const benchmark = new ChainBenchmark('ChainRule Inference Performance');
|
||
|
|
|
||
|
|
// Remove some relationships to create inference opportunities
|
||
|
|
const relationships = [];
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
const user = graph.users[i];
|
||
|
|
const group = graph.groups[Math.floor(Math.random() * Math.min(20, graph.groups.length))];
|
||
|
|
relationships.push({ user, group });
|
||
|
|
graph.arbiter.relationManager.removeRelation(user, 'member_of', group);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Chain without inference (baseline)
|
||
|
|
graph.arbiter.setRelationConfig('chain_no_inference', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
],
|
||
|
|
allowInference: false
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_no_inference', project, { noInfer: true });
|
||
|
|
}, 500, 'Chain (no inference)');
|
||
|
|
|
||
|
|
// Chain with basic inference
|
||
|
|
graph.arbiter.setRelationConfig('chain_basic_inference', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
],
|
||
|
|
allowInference: true
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_basic_inference', project);
|
||
|
|
}, 500, 'Chain (basic inference)');
|
||
|
|
|
||
|
|
// Chain with inference and reliability threshold
|
||
|
|
graph.arbiter.setRelationConfig('chain_reliable_inference', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
],
|
||
|
|
allowInference: true,
|
||
|
|
minReliability: 0.7
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_reliable_inference', project);
|
||
|
|
}, 500, 'Chain (high reliability inference)');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// Benchmark 7: Real-World Enterprise Scenarios
|
||
|
|
async function benchmarkEnterpriseScenarios() {
|
||
|
|
console.log('🏢 Benchmark 7: Real-World Enterprise Authorization Scenarios\n');
|
||
|
|
|
||
|
|
const graph = setupEnterpriseChainGraph('medium');
|
||
|
|
const benchmark = new ChainBenchmark('Enterprise Scenarios');
|
||
|
|
|
||
|
|
// Scenario 1: Budget Authorization (user → group → project → budget >= threshold)
|
||
|
|
graph.arbiter.setRelationConfig('budget_authorization', {
|
||
|
|
type: 'relational_comparator',
|
||
|
|
leftOperand: {
|
||
|
|
rule: {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
},
|
||
|
|
extractValue: true
|
||
|
|
},
|
||
|
|
rightOperand: {
|
||
|
|
rule: { type: 'direct', relation: 'has_value' },
|
||
|
|
extractValue: true
|
||
|
|
},
|
||
|
|
comparator: '>=',
|
||
|
|
fallbackBehavior: 'deny'
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add budget thresholds
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
const threshold = Math.floor(Math.random() * 1000000) + 50000;
|
||
|
|
graph.arbiter.addRelation(`threshold:${i}`, 'has_value', `value:${threshold}`, { value: threshold });
|
||
|
|
}
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const threshold = `threshold:${Math.floor(Math.random() * 50)}`;
|
||
|
|
return graph.arbiter.check(user, 'budget_authorization', threshold);
|
||
|
|
}, 500, 'Budget Authorization Chain');
|
||
|
|
|
||
|
|
// Scenario 2: Facility Access (user → department → facility)
|
||
|
|
graph.arbiter.setRelationConfig('facility_access', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'belongs_to', direction: 'out' },
|
||
|
|
{ relation: 'operates_in', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||
|
|
return graph.arbiter.check(user, 'facility_access', facility);
|
||
|
|
}, 1000, 'Facility Access Chain');
|
||
|
|
|
||
|
|
// Scenario 3: Resource Approval (user → group → project → resource)
|
||
|
|
graph.arbiter.setRelationConfig('resource_approval', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'uses_resource', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const resource = graph.resources[Math.floor(Math.random() * graph.resources.length)];
|
||
|
|
return graph.arbiter.check(user, 'resource_approval', resource.key);
|
||
|
|
}, 1000, 'Resource Approval Chain');
|
||
|
|
|
||
|
|
// Scenario 4: Cross-Division Access (complex multi-step authorization)
|
||
|
|
graph.arbiter.setRelationConfig('cross_division_access', {
|
||
|
|
union: [
|
||
|
|
{
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'belongs_to', direction: 'out' },
|
||
|
|
{ relation: 'part_of', direction: 'out' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'located_in', direction: 'out' },
|
||
|
|
{ relation: 'operates_in', direction: 'in' },
|
||
|
|
{ relation: 'part_of', direction: 'out' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||
|
|
return graph.arbiter.check(user, 'cross_division_access', `division:${division}`);
|
||
|
|
}, 1000, 'Cross-Division Access (Union)');
|
||
|
|
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Main benchmark runner
|
||
|
|
async function runChainRuleBenchmarks() {
|
||
|
|
console.log('🚀 Starting ChainRule Performance Benchmarks...\n');
|
||
|
|
|
||
|
|
try {
|
||
|
|
await benchmarkChainLength();
|
||
|
|
await benchmarkVsTraditional();
|
||
|
|
await benchmarkValueExtraction();
|
||
|
|
await benchmarkDirectionPatterns();
|
||
|
|
await benchmarkChainInference();
|
||
|
|
await benchmarkEnterpriseScenarios();
|
||
|
|
|
||
|
|
console.log('\n✅ All ChainRule benchmarks completed successfully!');
|
||
|
|
|
||
|
|
console.log('\n🚀 X-LARGE SCALE BENCHMARKS (xlarge)');
|
||
|
|
await benchmarkChainLengthScale('xlarge', 100);
|
||
|
|
await benchmarkVsTraditionalScale('xlarge', 100);
|
||
|
|
console.log('\n🚀 XX-LARGE SCALE BENCHMARKS (xxlarge)');
|
||
|
|
await benchmarkChainLengthScale('xxlarge', 50);
|
||
|
|
await benchmarkVsTraditionalScale('xxlarge', 50);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('❌ Benchmark failed:', error);
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run if this file is executed directly
|
||
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
|
|
runChainRuleBenchmarks();
|
||
|
|
}
|
||
|
|
|
||
|
|
export { runChainRuleBenchmarks };
|
||
|
|
|
||
|
|
// Add helper functions for large scale runs:
|
||
|
|
async function benchmarkChainLengthScale(scale, iterations) {
|
||
|
|
console.log(`\n📏 [${scale.toUpperCase()}] Chain Length Performance Impact`);
|
||
|
|
const graph = setupEnterpriseChainGraph(scale);
|
||
|
|
const benchmark = new ChainBenchmark(`Chain Length Performance (${scale})`);
|
||
|
|
// Baseline: Direct access (no chain)
|
||
|
|
graph.arbiter.setRelationConfig('direct_access', {
|
||
|
|
type: 'direct',
|
||
|
|
relation: 'can_access'
|
||
|
|
});
|
||
|
|
const directAccessPairs = [];
|
||
|
|
graph.users.forEach((user, i) => {
|
||
|
|
const numProjects = Math.floor(Math.random() * 3) + 1;
|
||
|
|
for (let j = 0; j < numProjects; j++) {
|
||
|
|
const project = graph.projects[(i * 7 + j) % graph.projects.length];
|
||
|
|
graph.arbiter.addRelation(user, 'can_access', project);
|
||
|
|
directAccessPairs.push({ user, project });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const pair = directAccessPairs[Math.floor(Math.random() * directAccessPairs.length)];
|
||
|
|
return graph.arbiter.check(pair.user, 'direct_access', pair.project, { noInfer: true });
|
||
|
|
}, iterations, 'Direct Access (baseline)');
|
||
|
|
// 2-step chain
|
||
|
|
graph.arbiter.setRelationConfig('chain_2_step', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_2_step', project, { noInfer: true });
|
||
|
|
}, iterations, '2-Step Chain (user→group→project)');
|
||
|
|
benchmark.report();
|
||
|
|
}
|
||
|
|
async function benchmarkVsTraditionalScale(scale, iterations) {
|
||
|
|
console.log(`\n⚔️ [${scale.toUpperCase()}] ChainRule vs Traditional Authorization`);
|
||
|
|
const graph = setupEnterpriseChainGraph(scale);
|
||
|
|
const benchmark = new ChainBenchmark(`ChainRule vs Traditional (${scale})`);
|
||
|
|
// Traditional ParentRule approach
|
||
|
|
graph.arbiter.setRelationConfig('traditional_parent', {
|
||
|
|
type: 'parent',
|
||
|
|
parentRelation: 'manages',
|
||
|
|
relation: 'member_of'
|
||
|
|
});
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'traditional_parent', project, { noInfer: true });
|
||
|
|
}, iterations, 'ParentRule (baseline)');
|
||
|
|
// Equivalent ChainRule approach
|
||
|
|
graph.arbiter.setRelationConfig('chain_equivalent', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
await benchmark.run(() => {
|
||
|
|
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||
|
|
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||
|
|
return graph.arbiter.check(user, 'chain_equivalent', project, { noInfer: true });
|
||
|
|
}, iterations, 'ChainRule (equivalent logic)');
|
||
|
|
benchmark.report();
|
||
|
|
}
|