rhizome-node/__tests__/performance/nested-resolution-performance.test.ts
Lentil Hoffman d7c4fda93e
refactor(resolver): overhaul plugin system and dependency handling
Core Changes:
- Completely rewrote CustomResolver reducer with dependency-ordered processing
- Enhanced plugin initialization with proper dependency injection
- Improved delta processing and property value tracking
- Added robust error handling for duplicate property IDs

Resolver Improvements:
- Updated to use new accumulator structure
- Implemented execution order processing for plugins
- Enhanced debug logging and error reporting
- Simplified TimestampResolver by removing unused initializer

Configuration Updates:
- Added TypeScript path aliases for test helpers
- Improved module resolution paths

Key Benefits:
- More robust plugin dependency management
- More efficient state updates
- Enhanced type safety
- Better error messages and debugging
- More consistent plugin initialization

This refactoring focuses on improving the robustness of the resolver,
especially around plugin lifecycle management and dependency handling.
The changes ensure better separation of concerns and more predictable
behavior when dealing with complex plugin dependencies.
2025-06-25 06:10:34 -05:00

337 lines
13 KiB
TypeScript

/**
* Performance tests for nested object resolution with large object graphs
*
* Tests performance characteristics of:
* - Large networks of interconnected entities
* - Deep nesting chains
* - Wide arrays of references
* - Circular reference handling at scale
*/
import Debug from 'debug';
import { RhizomeNode } from '@src/node';
import { createDelta } from '@src/core';
import { DefaultSchemaRegistry } from '@src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '@src/schema';
import { TypedCollectionImpl } from '@src/collections';
const debug = Debug('rz:test:nested-resolution-performance');
describe('Nested Object Resolution Performance', () => {
let node: RhizomeNode;
let schemaRegistry: DefaultSchemaRegistry;
beforeEach(() => {
node = new RhizomeNode();
schemaRegistry = new DefaultSchemaRegistry();
});
describe('Large Network Performance', () => {
test('should handle large networks of interconnected users efficiently', async () => {
// Create a schema for users with multiple relationship types
const networkUserSchema = SchemaBuilder
.create('network-user')
.name('Network User')
.property('name', PrimitiveSchemas.requiredString())
.property('friends', ArraySchemas.of(ReferenceSchemas.to('network-user-summary', 2)))
.property('followers', ArraySchemas.of(ReferenceSchemas.to('network-user-summary', 2)))
.property('mentor', ReferenceSchemas.to('network-user-summary', 2))
.required('name')
.build();
const networkUserSummarySchema = SchemaBuilder
.create('network-user-summary')
.name('Network User Summary')
.property('name', PrimitiveSchemas.requiredString())
.required('name')
.additionalProperties(false)
.build();
schemaRegistry.register(networkUserSchema);
schemaRegistry.register(networkUserSummarySchema);
const userCollection = new TypedCollectionImpl<{
name: string;
friends?: string[];
followers?: string[];
mentor?: string;
}>('users', networkUserSchema, schemaRegistry);
userCollection.rhizomeConnect(node);
const startSetup = performance.now();
// Create 100 users
const userCount = 100;
const userIds: string[] = [];
for (let i = 0; i < userCount; i++) {
const userId = `user${i}`;
userIds.push(userId);
await userCollection.put(userId, { name: `User ${i}` });
}
// Create a network where each user has 5-10 friends, 10-20 followers, and 1 mentor
for (let i = 0; i < userCount; i++) {
const userId = userIds[i];
// Add friends (5-10 random connections)
const friendCount = 5 + Math.floor(Math.random() * 6);
for (let j = 0; j < friendCount; j++) {
const friendIndex = Math.floor(Math.random() * userCount);
if (friendIndex !== i) {
const friendId = userIds[friendIndex];
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'friends', friendId, 'users')
.buildV1();
node.lossless.ingestDelta(friendshipDelta);
}
}
// Add followers (10-20 random connections)
const followerCount = 10 + Math.floor(Math.random() * 11);
for (let j = 0; j < followerCount; j++) {
const followerIndex = Math.floor(Math.random() * userCount);
if (followerIndex !== i) {
const followerId = userIds[followerIndex];
const followDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'followers', followerId, 'users')
.buildV1();
node.lossless.ingestDelta(followDelta);
}
}
// Add mentor (one per user, creating a hierarchy)
if (i > 0) {
const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure
const mentorId = userIds[mentorIndex];
const mentorshipDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'mentor', mentorId, 'users')
.buildV1();
node.lossless.ingestDelta(mentorshipDelta);
}
}
const setupTime = performance.now() - startSetup;
debug(`Setup time for ${userCount} users with relationships: ${setupTime.toFixed(2)}ms`);
// Test resolution performance for a user with many connections
const testUserId = userIds[50]; // Pick a user in the middle
const userViews = node.lossless.compose([testUserId]);
const userView = userViews[testUserId];
const startResolution = performance.now();
const nestedView = schemaRegistry.applySchemaWithNesting(
userView,
'network-user',
node.lossless,
{ maxDepth: 2 }
);
const resolutionTime = performance.now() - startResolution;
debug(`Resolution time for user with many connections: ${resolutionTime.toFixed(2)}ms`);
// Verify the resolution worked
expect(nestedView.id).toBe(testUserId);
expect(nestedView.schemaId).toBe('network-user');
// Performance assertions (adjust thresholds based on acceptable performance)
expect(setupTime).toBeLessThan(5000); // Setup should take less than 5 seconds
expect(resolutionTime).toBeLessThan(1000); // Resolution should take less than 1 second
// Verify we got some nested objects
const totalNestedObjects = Object.values(nestedView.nestedObjects).reduce(
(total, arr) => total + (arr?.length || 0), 0
);
debug('Total nested objects resolved: %o', totalNestedObjects);
// The test user should have friends, followers, and possibly a mentor
expect(Object.keys(nestedView.nestedObjects).length).toBeGreaterThan(0);
});
test('should handle deep nesting chains efficiently', async () => {
// Create a simple schema for chain testing
const chainUserSchema = SchemaBuilder
.create('chain-user')
.name('Chain User')
.property('name', PrimitiveSchemas.requiredString())
.property('next', ReferenceSchemas.to('chain-user-summary', 3))
.required('name')
.build();
const chainUserSummarySchema = SchemaBuilder
.create('chain-user-summary')
.name('Chain User Summary')
.property('name', PrimitiveSchemas.requiredString())
.required('name')
.additionalProperties(false)
.build();
schemaRegistry.register(chainUserSchema);
schemaRegistry.register(chainUserSummarySchema);
const userCollection = new TypedCollectionImpl<{
name: string;
next?: string;
}>('users', chainUserSchema, schemaRegistry);
userCollection.rhizomeConnect(node);
const startSetup = performance.now();
// Create a chain of 50 users
const chainLength = 50;
const userIds: string[] = [];
for (let i = 0; i < chainLength; i++) {
const userId = `chain-user${i}`;
userIds.push(userId);
await userCollection.put(userId, { name: `Chain User ${i}` });
}
// Link them in a chain
for (let i = 0; i < chainLength - 1; i++) {
const currentId = userIds[i];
const nextId = userIds[i + 1];
const linkDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(currentId, 'next', nextId, 'users')
.buildV1();
node.lossless.ingestDelta(linkDelta);
}
const setupTime = performance.now() - startSetup;
debug(`Setup time for chain of ${chainLength} users: ${setupTime.toFixed(2)}ms`);
// Test resolution from the start of the chain
const firstUserId = userIds[0];
const userViews = node.lossless.compose([firstUserId]);
const userView = userViews[firstUserId];
const startResolution = performance.now();
const nestedView = schemaRegistry.applySchemaWithNesting(
userView,
'chain-user',
node.lossless,
{ maxDepth: 5 } // Should resolve 5 levels deep
);
const resolutionTime = performance.now() - startResolution;
debug(`Resolution time for deep chain (maxDepth=5): ${resolutionTime.toFixed(2)}ms`);
// Verify the resolution worked and respected depth limits
expect(nestedView.id).toBe(firstUserId);
expect(nestedView.schemaId).toBe('chain-user');
// Performance assertions
expect(setupTime).toBeLessThan(2000); // Setup should take less than 2 seconds
expect(resolutionTime).toBeLessThan(500); // Resolution should take less than 500ms
// Verify depth was respected
let currentView = nestedView;
let depth = 0;
while (currentView.nestedObjects.next && currentView.nestedObjects.next.length > 0) {
currentView = currentView.nestedObjects.next[0];
depth++;
if (depth >= 10) break; // Prevent infinite loop
}
expect(depth).toBeLessThanOrEqual(5);
debug(`Actual resolved depth: ${depth}`);
});
test('should handle circular references in large graphs without performance degradation', async () => {
const circularUserSchema = SchemaBuilder
.create('circular-user')
.name('Circular User')
.property('name', PrimitiveSchemas.requiredString())
.property('connections', ArraySchemas.of(ReferenceSchemas.to('circular-user-summary', 3)))
.required('name')
.build();
const circularUserSummarySchema = SchemaBuilder
.create('circular-user-summary')
.name('Circular User Summary')
.property('name', PrimitiveSchemas.requiredString())
.required('name')
.additionalProperties(false)
.build();
schemaRegistry.register(circularUserSchema);
schemaRegistry.register(circularUserSummarySchema);
const userCollection = new TypedCollectionImpl<{
name: string;
connections?: string[];
}>('users', circularUserSchema, schemaRegistry);
userCollection.rhizomeConnect(node);
const startSetup = performance.now();
// Create 20 users
const userCount = 20;
const userIds: string[] = [];
for (let i = 0; i < userCount; i++) {
const userId = `circular-user${i}`;
userIds.push(userId);
await userCollection.put(userId, { name: `Circular User ${i}` });
}
// Create many circular connections - each user connects to 3 others
for (let i = 0; i < userCount; i++) {
const userId = userIds[i];
// Connect to next 3 users (wrapping around)
for (let j = 1; j <= 3; j++) {
const connectedIndex = (i + j) % userCount;
const connectedId = userIds[connectedIndex];
const connectionDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', userId, 'connections')
.addPointer('connections', connectedId)
.buildV1();
node.lossless.ingestDelta(connectionDelta);
}
}
const setupTime = performance.now() - startSetup;
debug(`Setup time for circular graph with ${userCount} users: ${setupTime.toFixed(2)}ms`);
// Test resolution performance with circular references
const testUserId = userIds[0];
const userViews = node.lossless.compose([testUserId]);
const userView = userViews[testUserId];
const startResolution = performance.now();
const nestedView = schemaRegistry.applySchemaWithNesting(
userView,
'circular-user',
node.lossless,
{ maxDepth: 3 }
);
const resolutionTime = performance.now() - startResolution;
debug(`Resolution time for circular graph (maxDepth=3): ${resolutionTime.toFixed(2)}ms`);
// Verify the resolution completed without hanging
expect(nestedView.id).toBe(testUserId);
expect(nestedView.schemaId).toBe('circular-user');
// Performance assertions - should handle circular references efficiently
expect(setupTime).toBeLessThan(2000);
expect(resolutionTime).toBeLessThan(1000); // Should complete in reasonable time despite cycles
// Verify we got some nested objects but didn't get stuck in infinite loops
expect(nestedView.nestedObjects.connections).toBeDefined();
if (nestedView.nestedObjects.connections) {
expect(nestedView.nestedObjects.connections.length).toBeGreaterThan(0);
expect(nestedView.nestedObjects.connections.length).toBeLessThanOrEqual(3);
}
debug(`Connections resolved: ${nestedView.nestedObjects.connections?.length || 0}`);
});
});
});