- Refactored delta creation in test files to use createDelta() pattern - Replaced direct Delta instantiations with fluent builder API - Updated relationship deltas to use setProperty with proper entity context - Ensured all tests pass with the new delta creation approach This is part of the ongoing effort to standardize on the DeltaBuilder API across the codebase for better consistency and maintainability.
346 lines
13 KiB
TypeScript
346 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';
|
|
|
|
const debug = Debug('rz:test:nested-resolution-performance');
|
|
import { Delta, createDelta } from '../src/core';
|
|
import { DefaultSchemaRegistry } from '../src/schema';
|
|
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema';
|
|
import { TypedCollectionImpl } from '../src/collections';
|
|
|
|
describe('Nested Object Resolution Performance', () => {
|
|
let node: RhizomeNode;
|
|
let schemaRegistry: DefaultSchemaRegistry;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
schemaRegistry = new DefaultSchemaRegistry();
|
|
});
|
|
|
|
describe('Large Network Performance', () => {
|
|
it('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.view([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);
|
|
});
|
|
|
|
it('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 = new Delta({
|
|
creator: node.config.creator,
|
|
host: node.config.peerId,
|
|
pointers: [
|
|
{ localContext: 'users', target: currentId, targetContext: 'next' },
|
|
{ localContext: 'next', target: nextId }
|
|
]
|
|
});
|
|
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.view([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}`);
|
|
});
|
|
|
|
it('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 = new Delta({
|
|
creator: node.config.creator,
|
|
host: node.config.peerId,
|
|
pointers: [
|
|
{ localContext: 'users', target: userId, targetContext: 'connections' },
|
|
{ localContext: 'connections', target: connectedId }
|
|
]
|
|
});
|
|
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.view([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}`);
|
|
});
|
|
});
|
|
}); |