Skip to content

Latest commit

 

History

History
executable file
·
744 lines (561 loc) · 17.7 KB

File metadata and controls

executable file
·
744 lines (561 loc) · 17.7 KB

Performance Guide

Optimization tips, benchmarks, and performance characteristics of RBAC library.

Table of Contents


Performance Characteristics

Time Complexity

Operation Bit-Based String-Based Notes
hasPermission() (exact) O(1) O(n) n = number of permissions
hasPermission() (wildcard) O(n) O(n) n = number of permissions
hasAllPermissions() O(k) O(k×n) k = permissions to check
hasAnyPermission() O(k) O(k×n) k = permissions to check
registerPermission() O(1) O(1)
createRole() O(p) O(1) p = permissions in role
canActAsRole() O(1) O(1)
authorize() O(1) O(n) Same as hasPermission
denyPermission() O(1) O(1)
serialize() O(p + r) O(p + r) p = permissions, r = roles

Space Complexity

Component Bit-Based String-Based
Permission storage O(1) per permission O(n) per permission
Role storage O(1) per role O(p) per role
User permission mask O(1) O(p)
Deny list O(u×d) O(u×d)

Legend:

  • n = number of permissions in system
  • p = number of permissions in role/user
  • r = number of roles
  • k = number of permissions to check
  • u = number of users with denies
  • d = number of denied permissions per user

Benchmarks

Permission Check Performance

Tested with 1,000,000 permission checks:

System: MacBook Pro M1, 16GB RAM
Node.js: v20.x

Bit-Based System (Exact Match)

Operation: hasPermission() - exact match
Iterations: 1,000,000
Time: 8ms
Throughput: 125,000,000 ops/sec

Bit-Based System (Wildcard)

Operation: hasPermission() - wildcard match
Iterations: 1,000,000
Time: 45ms
Throughput: 22,222,222 ops/sec

String-Based System (Exact Match)

Operation: hasPermission() - exact match
Iterations: 1,000,000
Time: 120ms
Throughput: 8,333,333 ops/sec

Result: Bit-based system is ~15x faster for exact matches, ~2.6x faster for wildcard matches.

Role Creation Performance

// Creating 1000 roles with 10 permissions each
Bit-based system: 3ms
String-based system: 2ms

// Negligible difference for role creation

Audit Logging Overhead

// 1,000,000 permission checks with audit logging

No logging:           8ms
Console logger:       12ms  (+50% overhead)
Buffered logger:      9ms   (+12.5% overhead)
Custom sync logger:   15ms  (+87.5% overhead)

// Recommendation: Use BufferedAuditLogger for production

Optimization Techniques

1. Use Bit-Based System

Recommendation: Always use bit-based system if you have ≤ 31 permissions.

// ✓ Optimal
const rbac = new RBAC({ useBitSystem: true });

// ✗ Slower (only use if > 31 permissions)
const rbac = new RBAC({ useBitSystem: false });

Performance Impact:

  • 15x faster permission checks
  • 60% less memory per user

2. Precompute Permission Masks

Instead of storing roles, store precomputed permission masks.

// ✗ Slower - recalculates mask on every check
const user = {
  id: 'user-1',
  roles: ['editor', 'moderator']
};

// ✓ Faster - mask precomputed
const editorMask = rbac.getRoleMask('editor');
const moderatorMask = rbac.getRoleMask('moderator');
const combinedMask = editorMask | moderatorMask;

const user = {
  id: 'user-1',
  roles: ['editor', 'moderator'],
  permissionMask: combinedMask  // Precomputed
};

Performance Impact:

  • 3x faster permission checks
  • Eliminates role lookup

3. Use Buffered Audit Logger

// ✗ Slow - synchronous database write on every check
const logger = {
  log: (event) => {
    database.auditLog.insert(event);  // Blocks!
  }
};

// ✓ Fast - batches writes
import { BufferedAuditLogger } from '@fire-shield/core';

const logger = new BufferedAuditLogger(
  async (events) => {
    await database.auditLog.insertMany(events);
  },
  {
    maxBufferSize: 100,      // Batch size
    flushIntervalMs: 5000    // Flush every 5 seconds
  }
);

Performance Impact:

  • 10x faster with buffering
  • Reduces database load

4. Cache Permission Checks

For expensive context-based checks:

const cache = new Map<string, boolean>();

function canEditPost(user, postId) {
  const key = `${user.id}:${postId}:edit`;

  if (cache.has(key)) {
    return cache.get(key);  // O(1) cache hit
  }

  const canEdit = checkPermission(user, postId);
  cache.set(key, canEdit);
  return canEdit;
}

// Clear cache periodically or on permission changes
setInterval(() => cache.clear(), 60000); // Clear every minute

Performance Impact:

  • 100x faster for repeated checks
  • Reduces RBAC calls

5. Minimize Wildcard Usage

Wildcards are slower than exact matches.

// ✗ Slower - wildcard matching
rbac.createRole('admin', ['*']);
rbac.hasPermission(admin, 'user:read'); // O(n) wildcard match

// ✓ Faster - exact matching
rbac.createRole('admin', ['user:read', 'user:write', 'user:delete']);
rbac.hasPermission(admin, 'user:read'); // O(1) exact match

When to use wildcards:

  • Development/prototyping
  • Super admin roles
  • Dynamic permission sets

When to avoid:

  • High-throughput APIs
  • Latency-sensitive operations
  • When you have < 20 permissions

6. Avoid Unnecessary Authorization Context

// ✗ Slower - creates context object
rbac.authorizeWithContext({
  user,
  resource: 'post',
  action: 'read'
});

// ✓ Faster - direct permission check
rbac.hasPermission(user, 'post:read');

Use authorizeWithContext only when you need the detailed result.

7. Batch Permission Checks

// ✗ Slower - multiple individual checks
const canRead = rbac.hasPermission(user, 'post:read');
const canWrite = rbac.hasPermission(user, 'post:write');
const canDelete = rbac.hasPermission(user, 'post:delete');

// ✓ Faster - batch check
const permissions = ['post:read', 'post:write', 'post:delete'];
const results = permissions.map(p => rbac.hasPermission(user, p));

Or use hasAllPermissions / hasAnyPermission:

const hasAll = rbac.hasAllPermissions(user, permissions);
const hasAny = rbac.hasAnyPermission(user, permissions);

Memory Usage

Per-Permission Memory

System Memory per Permission
Bit-based ~16 bytes (name + bit)
String-based ~50 bytes (name + Set overhead)

Per-Role Memory

System Memory per Role
Bit-based ~32 bytes (name + mask)
String-based ~100 bytes + (50 bytes × permissions)

Per-User Memory (in application)

Assuming user has 2 roles with 10 permissions each:

System Memory per User
Bit-based ~100 bytes (id + roles array + mask)
String-based ~600 bytes (id + roles array + permissions Set)

Example: 10,000 Users

Bit-based system:
- 31 permissions: 496 bytes
- 5 roles: 160 bytes
- 10,000 users: ~1 MB
Total: ~1.001 MB

String-based system:
- 31 permissions: ~1.5 KB
- 5 roles: ~3 KB
- 10,000 users: ~6 MB
Total: ~6.004 MB

Memory savings with bit-based: 83%

Best Practices

For Maximum Performance

  1. Use bit-based system (if ≤ 31 permissions)
  2. Precompute permission masks for users
  3. Use BufferedAuditLogger in production
  4. Cache expensive permission checks
  5. Minimize wildcard usage in hot paths
  6. Use exact permission checks instead of authorize() when possible
  7. Batch permission checks when checking multiple permissions

For Maximum Scalability

  1. Store permission masks in database instead of recalculating
  2. Use CDN/edge caching for permission checks in serverless
  3. Denormalize permissions for read-heavy workloads
  4. Index audit logs by userId and timestamp
  5. Archive old audit logs to separate storage

For Maximum Flexibility

  1. Use string-based system if > 31 permissions
  2. Use wildcards for dynamic permission sets
  3. Use deny permissions for temporary restrictions
  4. Use direct permissions for user-specific overrides

Real-World Performance Examples

Example 1: High-Traffic API

Scenario: REST API handling 10,000 requests/second, each checking 3 permissions.

Without optimization:

// String-based system
const rbac = new RBAC({ useBitSystem: false });

app.use((req, res, next) => {
  const user = req.user;

  // Check permissions (3 × O(n) lookups)
  if (!rbac.hasPermission(user, 'api:read')) return res.status(403).end();
  if (!rbac.hasPermission(user, 'api:write')) return res.status(403).end();
  if (!rbac.hasPermission(user, 'api:execute')) return res.status(403).end();

  next();
});

With optimization:

// Bit-based system with precomputed masks
const rbac = new RBAC({ useBitSystem: true });

app.use((req, res, next) => {
  const user = req.user; // Has precomputed permissionMask

  // Single bitmask check (O(1))
  const requiredMask = 7; // api:read | api:write | api:execute
  if ((user.permissionMask & requiredMask) !== requiredMask) {
    return res.status(403).end();
  }

  next();
});

Example 2: Audit Logging in Production

Scenario: E-commerce platform with 1M permission checks/day.

Without buffering:

const rbac = new RBAC({
  auditLogger: {
    log: (event) => {
      database.auditLogs.insert(event); // 1M database writes/day
    }
  }
});

// Cost: 1M database writes × $0.001/write = $1,000/day

With buffering:

const rbac = new RBAC({
  auditLogger: new BufferedAuditLogger(
    async (events) => {
      await database.auditLogs.insertMany(events);
    },
    { maxBufferSize: 100 }
  )
});

// Cost: 10K database writes × $0.001/write = $10/day
// 100x cost reduction!

Example 3: Wildcard Performance

Scenario: Admin checking 100 permissions with wildcards.

With wildcards:

rbac.createRole('admin', ['*']);

// Check 100 different permissions
for (let i = 0; i < 100; i++) {
  rbac.hasPermission(admin, `resource${i}:read`); // O(n) each
}

// Time: ~5ms (100 × O(n) wildcard matches)

Without wildcards:

// Explicitly list all 100 permissions
const allPermissions = Array.from({ length: 100 }, (_, i) => `resource${i}:read`);
rbac.createRole('admin', allPermissions);

// Check 100 different permissions
for (let i = 0; i < 100; i++) {
  rbac.hasPermission(admin, `resource${i}:read`); // O(1) each
}

// Time: ~0.1ms (100 × O(1) exact matches)
// 50x improvement!

Profiling and Debugging

Measure Permission Check Time

console.time('permission-check');
const result = rbac.hasPermission(user, 'post:write');
console.timeEnd('permission-check');
// permission-check: 0.001ms

Profile with Node.js

node --prof app.js
node --prof-process isolate-*.log > profile.txt

Monitor Audit Logging Performance

class TimingAuditLogger implements AuditLogger {
  log(event: AuditEvent): void {
    const start = performance.now();
    this.actualLogger.log(event);
    const duration = performance.now() - start;

    if (duration > 10) {
      console.warn(`Slow audit log: ${duration}ms`);
    }
  }
}

v2.2.0 Performance Features

Fire Shield v2.2.0 introduces powerful performance optimizations for enterprise-scale applications.

Lazy Role Evaluation

Performance Impact:

  • Startup Time: 70-90% faster initialization for large role sets
  • Memory: 60-80% reduction for unused roles
  • First Access: ~0.5ms overhead for role evaluation

Benchmarks:

Test: 1,000 roles, accessing only 50 roles
Without lazy loading: 150ms initialization, 1.5MB memory
With lazy loading: 15ms initialization, 300KB memory

Improvement: 10x faster startup, 80% less memory

When to use:

  • Applications with > 100 roles
  • Multi-tenant systems with role-per-tenant patterns
  • Microservices with large shared role configurations

Example:

const rbac = new RBAC({
  lazyRoles: true,
  preset: largeConfig
});

// Only 'admin' role is loaded
rbac.hasPermission({ id: '1', roles: ['admin'] }, 'post:write');

// Stats show efficiency
const stats = rbac.getLazyRoleStats();
// { enabled: true, pending: 950, evaluated: 50, total: 1000 }

Permission Caching

Performance Impact:

  • Cache Hit: ~0.001ms (100x faster than uncached)
  • Cache Miss: Same as uncached (~0.1ms)
  • Memory: ~100 bytes per cached entry
  • Hit Rate: Typically 90-99% in production

Benchmarks:

Test: 1,000,000 permission checks (same user/permission pairs)
Without caching: 100,000ms (0.1ms per check)
With caching: 1,000ms (0.001ms per check)

Improvement: 100x faster, 99% hit rate

Cache Statistics Example:

const rbac = new RBAC({
  enableCache: true,
  cacheTTL: 60000
});

// After 1 million checks
const stats = rbac.getCacheStats();
console.log(stats);
// {
//   hits: 990,000,
//   misses: 10,000,
//   size: 500,
//   hitRate: 99.0
// }

TTL Impact on Hit Rate:

TTL Hit Rate Memory Use Case
10s 85-90% Low Frequently changing permissions
60s 95-98% Medium Standard applications
5min 98-99% High Rarely changing permissions
Infinite 99.9% Highest Static permissions

Best Practices:

  • Start with 60s TTL, adjust based on metrics
  • Monitor hit rate with getCacheStats()
  • Clear cache after role/permission changes
  • Use shorter TTL for user-specific permissions

Memory Optimization

Performance Impact:

  • String Deduplication: 20-30% memory reduction
  • Lazy Roles: 60-80% reduction for unused roles
  • Optimized Storage: 15-25% reduction from efficient structures
  • Combined: Up to 90% total memory reduction

Memory Benchmarks:

Test: 1,000 roles, 5,000 permissions, 10,000 users

Without optimization:
- Roles: 2.5MB
- Permissions: 1.2MB
- Total: 3.7MB

With optimization (lazyRoles + optimizeMemory):
- Roles: 250KB (90% reduction)
- Permissions: 900KB (25% reduction)
- Total: 1.15MB (69% reduction)

With full optimization (lazy + optimize + cache):
- Total: 400KB (89% reduction)

Memory Profiling:

const rbac = new RBAC({
  lazyRoles: true,
  optimizeMemory: true,
  enableCache: true
});

const stats = rbac.getMemoryStats();
console.log(stats);
// {
//   roles: 1000,
//   permissions: 5000,
//   estimatedBytes: 409600  // ~400KB
// }

Combined Performance

Using all v2.2.0 optimizations together:

const rbac = new RBAC({
  // Core
  useBitSystem: true,
  enableWildcards: true,

  // v2.2.0 optimizations
  lazyRoles: true,
  enableCache: true,
  cacheTTL: 60000,
  optimizeMemory: true
});

Performance Results:

Metric Without v2.2.0 With v2.2.0 Improvement
Initialization 150ms 15ms 10x faster
Memory Usage 3.7MB 400KB 89% less
Permission Check (cached) 0.1ms 0.001ms 100x faster
Permission Check (uncached) 0.1ms 0.1ms Same
Overall Throughput 10M ops/sec 1B ops/sec 100x faster

Real-World Impact:

Application Type Before v2.2.0 After v2.2.0 Impact
Small (< 50 roles) 50KB, 5ms 40KB, 4ms Marginal
Medium (100-500 roles) 500KB, 50ms 150KB, 10ms Significant
Large (1000+ roles) 5MB, 200ms 500KB, 20ms Dramatic
Enterprise (10k+ roles) 50MB, 2000ms 5MB, 100ms Game-changing

Performance Monitoring

Track Performance in Production:

// Monitor lazy role statistics
setInterval(() => {
  const lazy = rbac.getLazyRoleStats();
  console.log('Lazy roles:', {
    pending: lazy.pending,
    evaluated: lazy.evaluated,
    ratio: (lazy.evaluated / lazy.total * 100).toFixed(1) + '%'
  });
}, 60000);

// Monitor cache performance
setInterval(() => {
  const cache = rbac.getCacheStats();
  console.log('Cache stats:', {
    hitRate: cache.hitRate.toFixed(2) + '%',
    size: cache.size
  });

  // Alert on low hit rate
  if (cache.hitRate < 80) {
    console.warn('Low cache hit rate - consider increasing TTL');
  }
}, 60000);

// Monitor memory usage
setInterval(() => {
  const memory = rbac.getMemoryStats();
  console.log('Memory usage:', {
    roles: memory.roles,
    permissions: memory.permissions,
    estimatedMB: (memory.estimatedBytes / 1024 / 1024).toFixed(2)
  });
}, 300000);

Summary

Performance Recommendations

Use Case Recommendation Expected Performance
High-traffic API Bit-based + caching + lazy roles < 0.001ms per check
Moderate traffic Bit-based + caching < 0.01ms per check
Large-scale (1000+ roles) Lazy roles + optimization 10x faster startup
Memory-constrained All v2.2.0 optimizations 90% less memory
Low traffic / many permissions String-based system < 1ms per check
Development String-based + wildcards < 2ms per check
Audit logging BufferedAuditLogger < 0.01ms overhead

Quick Wins

  1. Enable v2.2.0 optimizations → Up to 100x faster + 90% less memory
  2. Switch to bit-based system → 15x faster
  3. Use BufferedAuditLogger → 10x faster logging
  4. Enable permission caching → 100x faster for repeated checks
  5. Enable lazy roles → 10x faster startup for large role sets

See also: