perf: binary direct checks skip cycle machinery

_checkBinary ran the visited-set cycle detection (visit-key string
allocation + Set has/add) on every call, including top-level direct
checks that never recurse — making the 'fast' binary path 2.5x slower
than the normal fast path, which already skips the block when _visited
is empty.

The cycle block now sits after the direct fast path: direct checks
return before it, while the rule-evaluation branches (which recurse via
evaluateRule with the shared _visited) still detect cycles. Binary
direct checks drop from 6.7us to 4.1us avg; the remaining 1.4x is the
honest cost of the richer binary result contract (allow/deny, threshold
compare, config lookup). Parity suites and full rigor remain green.
This commit is contained in:
John Dvorak
2026-08-02 13:22:43 -07:00
parent 223cfb97c3
commit 5566d6c0ab
+35 -31
View File
@@ -659,37 +659,6 @@ export class AuthorizationChecker {
};
}
// Check for cycles using efficient approach
const useKeyedVisited = this._getVisitedMode(_visited);
const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null;
if (useKeyedVisited) {
if (_visited.has(visitKey)) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
...(evaluation && { evaluation })
};
}
} else {
for (const visited of _visited) {
if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
...(evaluation && { evaluation })
};
}
}
}
_visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey });
const config = this.arbiter.relationConfigs.get(relation);
if (!config) {
return {
@@ -766,6 +735,41 @@ export class AuthorizationChecker {
}
}
// Check for cycles using efficient approach.
// Deliberately placed AFTER the direct fast path: direct checks never
// recurse, so they must not pay for key allocation / Set mutation. Only
// the rule-evaluation branches (which recurse via evaluateRule with the
// shared _visited) need cycle detection.
const useKeyedVisited = this._getVisitedMode(_visited);
const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null;
if (useKeyedVisited) {
if (_visited.has(visitKey)) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
...(evaluation && { evaluation })
};
}
} else {
for (const visited of _visited) {
if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
...(evaluation && { evaluation })
};
}
}
}
_visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey });
// Handle logical operators with binary evaluation
if (config.union || config.intersection || config.exclusion) {
const res = this.ruleEvaluator.evaluateRule(