initial commit: @arbiter/core authorization engine with js-rigor hardening
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.
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
import { RelationStore } from '../../src/core/RelationStore.js';
|
||||
|
||||
const runPerf = process.env.RUN_PERF_TESTS === '1';
|
||||
const perfTest = runPerf ? test : test.skip;
|
||||
|
||||
describe('RelationStore Read Performance', () => {
|
||||
perfTest('sequential read speed comparison', () => {
|
||||
const numRelations = 100000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
// Add relations
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations);
|
||||
}
|
||||
|
||||
// Benchmark sequential reads
|
||||
const iterations = 10000;
|
||||
|
||||
const start1 = Date.now();
|
||||
let sum1 = 0;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const idx = i % store.size();
|
||||
const rel = store.get(idx);
|
||||
sum1 += rel.possibility;
|
||||
}
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
const start2 = Date.now();
|
||||
let sum2 = 0;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const idx = i % store.size();
|
||||
const rel = store.getRaw(idx);
|
||||
sum2 += rel.possibility;
|
||||
}
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
console.log(`Sequential read speed (${iterations} iterations):`);
|
||||
console.log(` - get() (full object): ${time1}ms (${(time1/iterations*1000).toFixed(4)} µs/op)`);
|
||||
console.log(` - getRaw() (typed arrays): ${time2}ms (${(time2/iterations*1000).toFixed(4)} µs/op)`);
|
||||
console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`);
|
||||
|
||||
// Both should give same result
|
||||
assert.ok(Math.abs(sum1 - sum2) < 1e-6);
|
||||
});
|
||||
|
||||
perfTest('random access speed', () => {
|
||||
const numRelations = 100000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations);
|
||||
}
|
||||
|
||||
const iterations = 10000;
|
||||
const indices = Array.from({length: iterations}, () =>
|
||||
Math.floor(Math.random() * store.size())
|
||||
);
|
||||
|
||||
const start1 = Date.now();
|
||||
let count1 = 0;
|
||||
for (const idx of indices) {
|
||||
const rel = store.get(idx);
|
||||
if (rel.possibility > 0.5) count1++;
|
||||
}
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
const start2 = Date.now();
|
||||
let count2 = 0;
|
||||
for (const idx of indices) {
|
||||
const rel = store.getRaw(idx);
|
||||
if (rel.possibility > 0.5) count2++;
|
||||
}
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
console.log(`Random access speed (${iterations} iterations):`);
|
||||
console.log(` - get() (full object): ${time1}ms (${(time1/iterations*1000).toFixed(4)} µs/op)`);
|
||||
console.log(` - getRaw() (typed arrays): ${time2}ms (${(time2/iterations*1000).toFixed(4)} µs/op)`);
|
||||
console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`);
|
||||
|
||||
assert.strictEqual(count1, count2);
|
||||
});
|
||||
|
||||
perfTest('findMatches performance', () => {
|
||||
const numRelations = 50000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i % 1000, 'rel', (i + 1) % 1000);
|
||||
}
|
||||
|
||||
const srcId = 500;
|
||||
const relId = store.getRelationId('rel');
|
||||
|
||||
const iterations = 1000;
|
||||
|
||||
const start = Date.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const matches = store.findMatches(srcId, relId, null);
|
||||
}
|
||||
const time = Date.now() - start;
|
||||
|
||||
console.log(`findMatches performance (${iterations} iterations):`);
|
||||
console.log(` - Time: ${time}ms (${(time/iterations).toFixed(4)} ms/op)`);
|
||||
console.log(` - Matches found: ${store.findMatches(srcId, relId, null).length}`);
|
||||
console.log(` - Avg matches/query: ${(time/iterations).toFixed(4)}ms`);
|
||||
|
||||
assert.ok(time < iterations * 10, 'findMatches should be reasonably fast');
|
||||
});
|
||||
|
||||
perfTest('forEach iteration speed', () => {
|
||||
const numRelations = 100000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations, {
|
||||
possibility: Math.random()
|
||||
});
|
||||
}
|
||||
|
||||
const start1 = Date.now();
|
||||
let sum1 = 0;
|
||||
store.forEach((rel) => {
|
||||
sum1 += rel.possibility;
|
||||
});
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
const start2 = Date.now();
|
||||
let sum2 = 0;
|
||||
store.forEachRaw((rel) => {
|
||||
sum2 += rel.possibility;
|
||||
});
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
console.log(`forEach iteration speed (${store.size()} relations):`);
|
||||
console.log(` - forEach() (full object): ${time1}ms`);
|
||||
console.log(` - forEachRaw() (typed arrays): ${time2}ms`);
|
||||
console.log(` - Speedup: ${(time1/time2).toFixed(2)}x ${time2 < time1 ? '(faster)' : '(slower)'}`);
|
||||
|
||||
assert.ok(Math.abs(sum1 - sum2) < 1e-6);
|
||||
});
|
||||
|
||||
perfTest('cache-friendly access pattern', () => {
|
||||
const numRelations = 100000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations, {
|
||||
possibility: Math.random(),
|
||||
reliability: Math.random()
|
||||
});
|
||||
}
|
||||
|
||||
// Access all src fields first (cache-friendly)
|
||||
const start1 = Date.now();
|
||||
let totalSrc = 0;
|
||||
for (let i = 0; i < store.size(); i++) {
|
||||
totalSrc += store.src[i];
|
||||
}
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
// Access possibility fields (different typed array)
|
||||
const start2 = Date.now();
|
||||
let totalPoss = 0;
|
||||
for (let i = 0; i < store.size(); i++) {
|
||||
totalPoss += store.possibility[i];
|
||||
}
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
console.log(`Cache-friendly field access (${store.size()} relations):`);
|
||||
console.log(` - src field access: ${time1}ms`);
|
||||
console.log(` - possibility field access: ${time2}ms`);
|
||||
console.log(` - Each operation: ${((time1+time2)/(store.size()*1000)).toFixed(4)} µs/element`);
|
||||
});
|
||||
|
||||
perfTest('pattern matching queries', () => {
|
||||
const numRelations = 50000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
// Create different relation types
|
||||
const relationTypes = ['owner', 'member', 'viewer', 'editor'];
|
||||
relationTypes.forEach(rel => store.getRelationId(rel));
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
const relType = relationTypes[i % relationTypes.length];
|
||||
store.add(i, relType, (i + 1) % 1000, {
|
||||
possibility: Math.random()
|
||||
});
|
||||
}
|
||||
|
||||
const queries = 1000;
|
||||
|
||||
// Query: Find all relations from a source
|
||||
const srcId = 100;
|
||||
const start1 = Date.now();
|
||||
let count1 = 0;
|
||||
for (let i = 0; i < queries; i++) {
|
||||
const matches = store.findMatches(srcId, null, null);
|
||||
count1 += matches.length;
|
||||
}
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
// Query: Find all relations of a specific type
|
||||
const ownerRelId = store.getRelationId('owner');
|
||||
const start2 = Date.now();
|
||||
let count2 = 0;
|
||||
for (let i = 0; i < queries; i++) {
|
||||
const matches = store.findMatches(null, ownerRelId, null);
|
||||
count2 += matches.length;
|
||||
}
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
console.log(`Pattern matching queries (${queries} queries):`);
|
||||
console.log(` - Find by source: ${time1}ms (${(time1/queries).toFixed(4)} ms/query)`);
|
||||
console.log(` - Find by relation type: ${time2}ms (${(time2/queries).toFixed(4)} ms/query)`);
|
||||
console.log(` - Results: source=${count1/queries} avg, type=${count2/queries} avg`);
|
||||
|
||||
assert.ok(count1 > 0, 'Should find relations by source');
|
||||
assert.ok(count2 > 0, 'Should find relations by type');
|
||||
});
|
||||
|
||||
perfTest('update operation speed', () => {
|
||||
const numRelations = 50000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations, {
|
||||
possibility: 0.5,
|
||||
reliability: 0.7
|
||||
});
|
||||
}
|
||||
|
||||
const updates = 10000;
|
||||
const start = Date.now();
|
||||
for (let i = 0; i < updates; i++) {
|
||||
const idx = i % store.size();
|
||||
store.update(idx, { possibility: 0.9 });
|
||||
}
|
||||
const time = Date.now() - start;
|
||||
|
||||
console.log(`Update operation speed (${updates} updates):`);
|
||||
console.log(` - Time: ${time}ms`);
|
||||
console.log(` - Avg per update: ${(time/updates).toFixed(4)} ms`);
|
||||
console.log(` - Updates/sec: ${(updates/time*1000).toFixed(0)}`);
|
||||
|
||||
// Verify updates worked - check that value changed
|
||||
const checkIdx = Math.floor(store.size() / 2);
|
||||
const rel = store.get(checkIdx);
|
||||
const beforeUpdate = store.get(0);
|
||||
assert.ok(rel.possibility !== beforeUpdate.possibility || store.size() < 2, 'Update should change value');
|
||||
});
|
||||
|
||||
perfTest('remove operation speed', () => {
|
||||
const numRelations = 50000;
|
||||
const store = new RelationStore(numRelations);
|
||||
|
||||
for (let i = 0; i < numRelations; i++) {
|
||||
store.add(i, 'relation', (i + numRelations) % numRelations);
|
||||
}
|
||||
|
||||
const initialSize = store.size();
|
||||
const removes = 10000;
|
||||
const start = Date.now();
|
||||
for (let i = 0; i < removes; i++) {
|
||||
const idx = Math.floor(Math.random() * store.size());
|
||||
store.remove(idx);
|
||||
}
|
||||
const time = Date.now() - start;
|
||||
const finalSize = store.size();
|
||||
const actualRemoves = initialSize - finalSize;
|
||||
|
||||
console.log(`Remove operation speed (${removes} attempted):`);
|
||||
console.log(` - Time: ${time}ms`);
|
||||
console.log(` - Avg per remove: ${(time/removes).toFixed(4)} ms`);
|
||||
console.log(` - Actual removes: ${actualRemoves} (deduplication)`);
|
||||
console.log(` - Removes/sec: ${(actualRemoves/time*1000).toFixed(0)}`);
|
||||
|
||||
assert.strictEqual(finalSize, initialSize - removes);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user