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.
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
/**
|
|
* Test performance of different reachability indexing strategies
|
|
*/
|
|
|
|
import { test as _test } from 'node:test';
|
|
const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip;
|
|
import assert from 'node:assert/strict';
|
|
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
|
|
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
|
|
|
test('compares reachability strategy performance', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('🔍 Comparing reachability strategy performance...');
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph('enterprise');
|
|
|
|
// Test different strategies
|
|
const strategies = ['auto', 'twohop', 'treecover', 'hybrid'];
|
|
const results = {};
|
|
|
|
for (const strategy of strategies) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Testing strategy: ${strategy}`);
|
|
|
|
// Create new arbiter for each strategy
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
// Initialize reachability checker with specific strategy
|
|
await arbiter.reachabilityChecker.initialize({
|
|
strategy: strategy,
|
|
twoHopOptions: { maxNodes: 1000 },
|
|
treeCoverOptions: { maxTrees: 3 }
|
|
});
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a chain rule
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
],
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Find actual chain paths
|
|
const actualChains = [];
|
|
const roleMemberships = graphData.relations.filter(r =>
|
|
r.relation === 'member_of' && r.dst.startsWith('role:')
|
|
);
|
|
|
|
for (const membership of roleMemberships) {
|
|
const userId = membership.src;
|
|
const roleId = membership.dst;
|
|
|
|
const roleReadPermissions = graphData.relations.filter(r =>
|
|
r.relation === 'can_read' && r.src === roleId
|
|
);
|
|
|
|
for (const permission of roleReadPermissions) {
|
|
actualChains.push({
|
|
user: userId,
|
|
object: permission.dst
|
|
});
|
|
}
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${actualChains.length} chain paths`);
|
|
|
|
// Test performance
|
|
const start = Date.now();
|
|
let queryCount = 0;
|
|
let positiveResults = 0;
|
|
const end = start + 2000; // 2 seconds
|
|
|
|
while (Date.now() < end) {
|
|
for (const chain of actualChains) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(chain.user),
|
|
chain.user,
|
|
arbiter.nodeIdByKey.get(chain.object),
|
|
chain.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_role',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveResults++;
|
|
}
|
|
queryCount++;
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - start;
|
|
const qps = (queryCount / duration) * 1000;
|
|
const positiveRate = (positiveResults / queryCount) * 100;
|
|
|
|
results[strategy] = {
|
|
qps: qps,
|
|
queryCount: queryCount,
|
|
positiveRate: positiveRate,
|
|
duration: duration,
|
|
cacheSize: rule.chainResultCache.size
|
|
};
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${queryCount} queries, ${positiveRate.toFixed(1)}% positive)`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results`);
|
|
|
|
// Get reachability stats
|
|
const reachabilityStats = arbiter.reachabilityChecker.getStats();
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Reachability stats:`, reachabilityStats);
|
|
}
|
|
|
|
// Compare results
|
|
if (process.env.TEST_DEBUG === '1') console.log(' 📊 Strategy comparison:');
|
|
const sortedResults = Object.entries(results).sort((a, b) => b[1].qps - a[1].qps);
|
|
|
|
sortedResults.forEach(([strategy, result], index) => {
|
|
const rank = index + 1;
|
|
const improvement = index === 0 ? '🏆' : `-${((sortedResults[0][1].qps - result.qps) / sortedResults[0][1].qps * 100).toFixed(1)}%`;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS ${improvement}`);
|
|
});
|
|
|
|
// Verify that we have results
|
|
assert.ok(Object.keys(results).length > 0, 'Should have results for all strategies');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Reachability strategy comparison completed');
|
|
});
|
|
|
|
test('measures strategy performance with different graph sizes', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📈 Testing strategy performance with different graph sizes...');
|
|
|
|
const graphSizes = ['small', 'medium', 'enterprise'];
|
|
const strategies = ['auto', 'twohop', 'treecover', 'hybrid'];
|
|
const results = {};
|
|
|
|
for (const size of graphSizes) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Testing ${size} graph...`);
|
|
results[size] = {};
|
|
|
|
for (const strategy of strategies) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Strategy: ${strategy}`);
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph(size);
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
// Initialize reachability checker
|
|
await arbiter.reachabilityChecker.initialize({
|
|
strategy: strategy,
|
|
twoHopOptions: { maxNodes: 1000 },
|
|
treeCoverOptions: { maxTrees: 3 }
|
|
});
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a chain rule
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
],
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Find actual chain paths
|
|
const actualChains = [];
|
|
const roleMemberships = graphData.relations.filter(r =>
|
|
r.relation === 'member_of' && r.dst.startsWith('role:')
|
|
);
|
|
|
|
for (const membership of roleMemberships) {
|
|
const userId = membership.src;
|
|
const roleId = membership.dst;
|
|
|
|
const roleReadPermissions = graphData.relations.filter(r =>
|
|
r.relation === 'can_read' && r.src === roleId
|
|
);
|
|
|
|
for (const permission of roleReadPermissions) {
|
|
actualChains.push({
|
|
user: userId,
|
|
object: permission.dst
|
|
});
|
|
}
|
|
}
|
|
|
|
// Test performance
|
|
const start = Date.now();
|
|
let queryCount = 0;
|
|
let positiveResults = 0;
|
|
const end = start + 1000; // 1 second
|
|
|
|
while (Date.now() < end) {
|
|
for (const chain of actualChains) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(chain.user),
|
|
chain.user,
|
|
arbiter.nodeIdByKey.get(chain.object),
|
|
chain.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_role',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveResults++;
|
|
}
|
|
queryCount++;
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - start;
|
|
const qps = (queryCount / duration) * 1000;
|
|
const positiveRate = (positiveResults / queryCount) * 100;
|
|
|
|
results[size][strategy] = {
|
|
qps: qps,
|
|
queryCount: queryCount,
|
|
positiveRate: positiveRate,
|
|
chainCount: actualChains.length
|
|
};
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${actualChains.length} chains)`);
|
|
}
|
|
}
|
|
|
|
// Compare results across graph sizes
|
|
if (process.env.TEST_DEBUG === '1') console.log(' 📊 Results by graph size:');
|
|
for (const size of graphSizes) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${size}:`);
|
|
const sortedResults = Object.entries(results[size]).sort((a, b) => b[1].qps - a[1].qps);
|
|
sortedResults.forEach(([strategy, result], index) => {
|
|
const rank = index + 1;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS (${result.chainCount} chains)`);
|
|
});
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Graph size strategy comparison completed');
|
|
});
|
|
|
|
test('measures strategy performance with different chain lengths', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('🔗 Testing strategy performance with different chain lengths...');
|
|
|
|
const generator = new BigGraphGenerator();
|
|
const graphData = generator.generateGraph('enterprise');
|
|
|
|
const strategies = ['auto', 'twohop', 'treecover', 'hybrid'];
|
|
const chainLengths = [2, 3, 4];
|
|
const results = {};
|
|
|
|
for (const length of chainLengths) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Testing chain length: ${length}`);
|
|
results[length] = {};
|
|
|
|
for (const strategy of strategies) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Strategy: ${strategy}`);
|
|
|
|
const arbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
// Initialize reachability checker
|
|
await arbiter.reachabilityChecker.initialize({
|
|
strategy: strategy,
|
|
twoHopOptions: { maxNodes: 1000 },
|
|
treeCoverOptions: { maxTrees: 3 }
|
|
});
|
|
|
|
const rule = new ChainRule(arbiter);
|
|
|
|
// Create a chain rule with specified length
|
|
const chainRule = {
|
|
type: 'chain',
|
|
steps: Array.from({ length }, (_, i) => ({
|
|
relation: i === 0 ? 'member_of' : 'can_read',
|
|
direction: 'out'
|
|
})),
|
|
collectValues: false,
|
|
valueAggregation: 'sum'
|
|
};
|
|
|
|
// Find actual chain paths
|
|
const actualChains = [];
|
|
const roleMemberships = graphData.relations.filter(r =>
|
|
r.relation === 'member_of' && r.dst.startsWith('role:')
|
|
);
|
|
|
|
for (const membership of roleMemberships) {
|
|
const userId = membership.src;
|
|
const roleId = membership.dst;
|
|
|
|
const roleReadPermissions = graphData.relations.filter(r =>
|
|
r.relation === 'can_read' && r.src === roleId
|
|
);
|
|
|
|
for (const permission of roleReadPermissions) {
|
|
actualChains.push({
|
|
user: userId,
|
|
object: permission.dst
|
|
});
|
|
}
|
|
}
|
|
|
|
// Test performance
|
|
const start = Date.now();
|
|
let queryCount = 0;
|
|
let positiveResults = 0;
|
|
const end = start + 1000; // 1 second
|
|
|
|
while (Date.now() < end) {
|
|
for (const chain of actualChains) {
|
|
const result = rule.evaluate(
|
|
arbiter.nodeIdByKey.get(chain.user),
|
|
chain.user,
|
|
arbiter.nodeIdByKey.get(chain.object),
|
|
chain.object,
|
|
chainRule,
|
|
new Set(),
|
|
'can_read_via_role',
|
|
{}
|
|
);
|
|
|
|
if (result.possibility > 0) {
|
|
positiveResults++;
|
|
}
|
|
queryCount++;
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - start;
|
|
const qps = (queryCount / duration) * 1000;
|
|
const positiveRate = (positiveResults / queryCount) * 100;
|
|
|
|
results[length][strategy] = {
|
|
qps: qps,
|
|
queryCount: queryCount,
|
|
positiveRate: positiveRate,
|
|
chainCount: actualChains.length
|
|
};
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${strategy}: ${qps.toFixed(2)} QPS (${actualChains.length} chains)`);
|
|
}
|
|
}
|
|
|
|
// Compare results across chain lengths
|
|
if (process.env.TEST_DEBUG === '1') console.log(' 📊 Results by chain length:');
|
|
for (const length of chainLengths) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Length ${length}:`);
|
|
const sortedResults = Object.entries(results[length]).sort((a, b) => b[1].qps - a[1].qps);
|
|
sortedResults.forEach(([strategy, result], index) => {
|
|
const rank = index + 1;
|
|
if (process.env.TEST_DEBUG === '1') console.log(` ${rank}. ${strategy}: ${result.qps.toFixed(2)} QPS (${result.chainCount} chains)`);
|
|
});
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Chain length strategy comparison completed');
|
|
});
|