660 lines
19 KiB
TypeScript
660 lines
19 KiB
TypeScript
|
|
/**
|
||
|
|
* Batch Operations Test Suite for Brainy v3.0
|
||
|
|
*
|
||
|
|
* Comprehensive testing of batch operations including:
|
||
|
|
* - addMany: Bulk entity creation
|
||
|
|
* - deleteMany: Bulk deletion
|
||
|
|
* - Performance validation at scale
|
||
|
|
* - Memory efficiency testing
|
||
|
|
* - Error recovery scenarios
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import { Brainy } from '../../src/brainy'
|
||
|
|
import { performance } from 'perf_hooks'
|
||
|
|
|
||
|
|
interface BatchMetrics {
|
||
|
|
operation: string
|
||
|
|
totalItems: number
|
||
|
|
successCount: number
|
||
|
|
failureCount: number
|
||
|
|
duration: number
|
||
|
|
throughput: number
|
||
|
|
memoryUsed: number
|
||
|
|
errors: string[]
|
||
|
|
}
|
||
|
|
|
||
|
|
class BatchMetricsCollector {
|
||
|
|
private metrics: BatchMetrics[] = []
|
||
|
|
|
||
|
|
recordBatch(
|
||
|
|
operation: string,
|
||
|
|
totalItems: number,
|
||
|
|
results: any[],
|
||
|
|
duration: number,
|
||
|
|
memoryUsed: number
|
||
|
|
): BatchMetrics {
|
||
|
|
const successCount = results.filter(r =>
|
||
|
|
r?.status === 'fulfilled' || r === true || (r && !r.error)
|
||
|
|
).length
|
||
|
|
const failureCount = totalItems - successCount
|
||
|
|
const errors = results
|
||
|
|
.filter(r => r?.status === 'rejected' || r?.error)
|
||
|
|
.map(r => r?.reason?.message || r?.error || 'Unknown error')
|
||
|
|
.slice(0, 5) // Limit to first 5 errors
|
||
|
|
|
||
|
|
const metric: BatchMetrics = {
|
||
|
|
operation,
|
||
|
|
totalItems,
|
||
|
|
successCount,
|
||
|
|
failureCount,
|
||
|
|
duration,
|
||
|
|
throughput: (totalItems / duration) * 1000,
|
||
|
|
memoryUsed,
|
||
|
|
errors
|
||
|
|
}
|
||
|
|
|
||
|
|
this.metrics.push(metric)
|
||
|
|
return metric
|
||
|
|
}
|
||
|
|
|
||
|
|
getMetrics(): BatchMetrics[] {
|
||
|
|
return this.metrics
|
||
|
|
}
|
||
|
|
|
||
|
|
generateReport(): void {
|
||
|
|
console.log('\n=== Batch Operations Performance Report ===\n')
|
||
|
|
console.table(this.metrics.map(m => ({
|
||
|
|
Operation: m.operation,
|
||
|
|
'Total Items': m.totalItems,
|
||
|
|
'Success': `${m.successCount}/${m.totalItems} (${((m.successCount/m.totalItems)*100).toFixed(1)}%)`,
|
||
|
|
'Duration (ms)': m.duration.toFixed(2),
|
||
|
|
'Throughput (items/s)': m.throughput.toFixed(0),
|
||
|
|
'Memory (MB)': (m.memoryUsed / 1024 / 1024).toFixed(2),
|
||
|
|
'Errors': m.errors.length > 0 ? m.errors[0] : 'None'
|
||
|
|
})))
|
||
|
|
|
||
|
|
// Summary statistics
|
||
|
|
const totalOps = this.metrics.reduce((sum, m) => sum + m.totalItems, 0)
|
||
|
|
const totalSuccess = this.metrics.reduce((sum, m) => sum + m.successCount, 0)
|
||
|
|
const avgThroughput = this.metrics.reduce((sum, m) => sum + m.throughput, 0) / this.metrics.length
|
||
|
|
|
||
|
|
console.log('\nSummary:')
|
||
|
|
console.log(`Total Operations: ${totalOps}`)
|
||
|
|
console.log(`Success Rate: ${((totalSuccess/totalOps)*100).toFixed(2)}%`)
|
||
|
|
console.log(`Average Throughput: ${avgThroughput.toFixed(0)} items/sec`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Batch Operations - Public API Testing', () => {
|
||
|
|
let brainy: Brainy
|
||
|
|
let metricsCollector: BatchMetricsCollector
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
brainy = new Brainy({
|
||
|
|
storage: { type: 'memory' }
|
||
|
|
})
|
||
|
|
await brainy.init()
|
||
|
|
metricsCollector = new BatchMetricsCollector()
|
||
|
|
})
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await brainy.close()
|
||
|
|
if (global.gc) global.gc()
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('addMany - Bulk Entity Creation', () => {
|
||
|
|
it('should add 100 entities efficiently', async () => {
|
||
|
|
const entities = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Batch entity ${i}: Test content for bulk operations`,
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: {
|
||
|
|
batchIndex: i,
|
||
|
|
batchId: 'batch-100',
|
||
|
|
timestamp: Date.now()
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
|
||
|
|
const startMemory = process.memoryUsage().heapUsed
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const result = await brainy.addMany({ items: entities })
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
const memoryUsed = process.memoryUsage().heapUsed - startMemory
|
||
|
|
|
||
|
|
const metric = metricsCollector.recordBatch(
|
||
|
|
'addMany-100',
|
||
|
|
entities.length,
|
||
|
|
result.successful,
|
||
|
|
duration,
|
||
|
|
memoryUsed
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(result.successful).toHaveLength(100)
|
||
|
|
expect(result.failed).toHaveLength(0)
|
||
|
|
expect(metric.throughput).toBeGreaterThan(100) // > 100 items/sec
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle 1,000 entities with proper batching', async () => {
|
||
|
|
const entities = Array(1000).fill(null).map((_, i) => ({
|
||
|
|
data: `Entity ${i}: ${' '.repeat(100)}`, // ~100 bytes each
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: {
|
||
|
|
index: i,
|
||
|
|
category: `cat-${i % 10}`,
|
||
|
|
tags: [`tag-${i % 5}`, `tag-${i % 7}`]
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
|
||
|
|
const startMemory = process.memoryUsage().heapUsed
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const result = await brainy.addMany({
|
||
|
|
items: entities,
|
||
|
|
chunkSize: 100, // Process in batches
|
||
|
|
parallel: true
|
||
|
|
})
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
const memoryUsed = process.memoryUsage().heapUsed - startMemory
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
'addMany-1k',
|
||
|
|
entities.length,
|
||
|
|
result.successful,
|
||
|
|
duration,
|
||
|
|
memoryUsed
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(result.successful.length).toBe(1000)
|
||
|
|
expect(result.failed.length).toBe(0)
|
||
|
|
expect(duration).toBeLessThan(10000) // < 10 seconds
|
||
|
|
|
||
|
|
// Verify data integrity
|
||
|
|
const sampleIds = result.successful.slice(0, 10)
|
||
|
|
for (const id of sampleIds) {
|
||
|
|
const entity = await brainy.get(id)
|
||
|
|
expect(entity).toBeDefined()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle 10,000 entities with memory efficiency', async () => {
|
||
|
|
const chunkSize = 1000
|
||
|
|
const totalEntities = 10000
|
||
|
|
let allSuccessful: string[] = []
|
||
|
|
let allFailed: any[] = []
|
||
|
|
|
||
|
|
const startMemory = process.memoryUsage().heapUsed
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Process in chunks to avoid memory issues
|
||
|
|
for (let chunk = 0; chunk < totalEntities; chunk += chunkSize) {
|
||
|
|
const entities = Array(Math.min(chunkSize, totalEntities - chunk))
|
||
|
|
.fill(null)
|
||
|
|
.map((_, i) => ({
|
||
|
|
data: `Chunk entity ${chunk + i}`,
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: { globalIndex: chunk + i }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const result = await brainy.addMany({ items: entities })
|
||
|
|
allSuccessful.push(...result.successful)
|
||
|
|
allFailed.push(...result.failed)
|
||
|
|
|
||
|
|
// Check memory usage
|
||
|
|
const currentMemory = process.memoryUsage().heapUsed
|
||
|
|
const memoryGrowth = currentMemory - startMemory
|
||
|
|
expect(memoryGrowth).toBeLessThan(500 * 1024 * 1024) // < 500MB total
|
||
|
|
}
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
const finalMemory = process.memoryUsage().heapUsed - startMemory
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
'addMany-10k',
|
||
|
|
totalEntities,
|
||
|
|
allSuccessful,
|
||
|
|
duration,
|
||
|
|
finalMemory
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(allSuccessful.length).toBe(10000)
|
||
|
|
expect(allFailed.length).toBe(0)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle partial failures gracefully', async () => {
|
||
|
|
const entities = [
|
||
|
|
{ data: 'Valid entity 1', type: 'document' as const },
|
||
|
|
{ data: '', type: 'document' as const }, // Invalid: empty data
|
||
|
|
{ data: 'Valid entity 2', type: 'document' as const },
|
||
|
|
{ data: null as any, type: 'document' as const }, // Invalid: null data
|
||
|
|
{ data: 'Valid entity 3', type: 'document' as const },
|
||
|
|
]
|
||
|
|
|
||
|
|
const result = await brainy.addMany({
|
||
|
|
items: entities,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Should process valid entities even if some fail
|
||
|
|
expect(result.successful.length).toBeGreaterThanOrEqual(3)
|
||
|
|
expect(result.failed.length).toBeGreaterThanOrEqual(0) // May or may not validate
|
||
|
|
|
||
|
|
// Verify valid entities were added
|
||
|
|
for (const id of result.successful) {
|
||
|
|
const entity = await brainy.get(id)
|
||
|
|
expect(entity).toBeDefined()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should support concurrent batch operations', async () => {
|
||
|
|
const batches = Array(5).fill(null).map((_, batchIdx) =>
|
||
|
|
Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Batch ${batchIdx} Entity ${i}`,
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: { batchId: batchIdx, index: i }
|
||
|
|
}))
|
||
|
|
)
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Execute batches concurrently
|
||
|
|
const results = await Promise.all(
|
||
|
|
batches.map(entities => brainy.addMany({ items: entities }))
|
||
|
|
)
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
expect(results).toHaveLength(5)
|
||
|
|
const totalSuccess = results.reduce((sum, r) => sum + r.successful.length, 0)
|
||
|
|
expect(totalSuccess).toBe(500)
|
||
|
|
expect(duration).toBeLessThan(5000) // Concurrent should be faster
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
let testIds: string[] = []
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Create test entities
|
||
|
|
const entities = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Original content ${i}`,
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: { version: 1, index: i }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const result = await brainy.addMany({ items: entities })
|
||
|
|
testIds = result.successful
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should update multiple entities by IDs', async () => {
|
||
|
|
const updates = testIds.slice(0, 50).map(id => ({
|
||
|
|
id,
|
||
|
|
updates: {
|
||
|
|
metadata: {
|
||
|
|
version: 2,
|
||
|
|
updatedAt: Date.now()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const results = await Promise.all(
|
||
|
|
updates.map(u => brainy.update({ id: u.id, ...u.updates }))
|
||
|
|
)
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
updates.length,
|
||
|
|
results,
|
||
|
|
duration,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
// Verify updates
|
||
|
|
const sample = await brainy.get(testIds[0])
|
||
|
|
expect(sample?.metadata?.version).toBe(2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should update entities matching criteria', async () => {
|
||
|
|
// Update all entities with index < 20
|
||
|
|
const targetIds = testIds.slice(0, 20)
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const results = await Promise.all(
|
||
|
|
targetIds.map(id =>
|
||
|
|
brainy.update({
|
||
|
|
id,
|
||
|
|
metadata: {
|
||
|
|
status: 'updated',
|
||
|
|
processedAt: Date.now()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
targetIds.length,
|
||
|
|
results,
|
||
|
|
duration,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
// Verify updates
|
||
|
|
for (const id of targetIds.slice(0, 5)) {
|
||
|
|
const entity = await brainy.get(id)
|
||
|
|
expect(entity?.metadata?.status).toBe('updated')
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('deleteMany - Bulk Deletion', () => {
|
||
|
|
let testIds: string[] = []
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Create test entities
|
||
|
|
const entities = Array(200).fill(null).map((_, i) => ({
|
||
|
|
data: `Delete test ${i}`,
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: { deleteGroup: i % 4 }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const result = await brainy.addMany({ items: entities })
|
||
|
|
testIds = result.successful
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should delete multiple entities by IDs', async () => {
|
||
|
|
const toDelete = testIds.slice(0, 100)
|
||
|
|
|
||
|
|
const startMemory = process.memoryUsage().heapUsed
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Use deleteMany if available, otherwise individual deletes
|
||
|
|
let results: any[]
|
||
|
|
if ('deleteMany' in brainy && typeof brainy.deleteMany === 'function') {
|
||
|
|
const result = await brainy.deleteMany({ ids: toDelete })
|
||
|
|
results = result.successful
|
||
|
|
} else {
|
||
|
|
results = await Promise.all(
|
||
|
|
toDelete.map(id => brainy.delete(id))
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
const memoryUsed = process.memoryUsage().heapUsed - startMemory
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
'deleteMany-100',
|
||
|
|
toDelete.length,
|
||
|
|
results,
|
||
|
|
duration,
|
||
|
|
memoryUsed
|
||
|
|
)
|
||
|
|
|
||
|
|
// Verify deletion
|
||
|
|
for (const id of toDelete.slice(0, 10)) {
|
||
|
|
const entity = await brainy.get(id)
|
||
|
|
expect(entity).toBeNull()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remaining entities should still exist
|
||
|
|
const remaining = testIds.slice(100, 110)
|
||
|
|
for (const id of remaining) {
|
||
|
|
const entity = await brainy.get(id)
|
||
|
|
expect(entity).toBeDefined()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle large-scale deletion efficiently', async () => {
|
||
|
|
// Create a large dataset
|
||
|
|
const largeDataset = Array(1000).fill(null).map((_, i) => ({
|
||
|
|
data: `Large scale delete test ${i}`,
|
||
|
|
type: 'document' as const
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brainy.addMany({ items: largeDataset })
|
||
|
|
const idsToDelete = addResult.successful
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Delete in batches
|
||
|
|
const batchSize = 100
|
||
|
|
for (let i = 0; i < idsToDelete.length; i += batchSize) {
|
||
|
|
const batch = idsToDelete.slice(i, i + batchSize)
|
||
|
|
await Promise.all(batch.map(id => brainy.delete(id)))
|
||
|
|
}
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
'deleteMany-1k',
|
||
|
|
idsToDelete.length,
|
||
|
|
idsToDelete.map(() => true),
|
||
|
|
duration,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(duration).toBeLessThan(10000) // < 10 seconds
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
let nodeIds: string[] = []
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Create nodes for relationships
|
||
|
|
const nodes = Array(50).fill(null).map((_, i) => ({
|
||
|
|
data: `Node ${i}`,
|
||
|
|
type: 'thing' as const,
|
||
|
|
metadata: { nodeIndex: i }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const result = await brainy.addMany({ items: nodes })
|
||
|
|
nodeIds = result.successful
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should create multiple relationships efficiently', async () => {
|
||
|
|
const relationships: Array<{from: string, to: string, type: string}> = []
|
||
|
|
|
||
|
|
// Create a connected graph
|
||
|
|
for (let i = 0; i < nodeIds.length - 1; i++) {
|
||
|
|
relationships.push({
|
||
|
|
from: nodeIds[i],
|
||
|
|
to: nodeIds[i + 1],
|
||
|
|
type: 'connected_to'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const results = await Promise.all(
|
||
|
|
relationships.map(r =>
|
||
|
|
brainy.relate({ from: r.from, to: r.to, type: r.type as any })
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
relationships.length,
|
||
|
|
results,
|
||
|
|
duration,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(results.every(r => typeof r === 'string')).toBe(true) // relate returns ID
|
||
|
|
expect(duration).toBeLessThan(5000)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should create complex graph structures', async () => {
|
||
|
|
// Create a fully connected subgraph (first 10 nodes)
|
||
|
|
const subgraph = nodeIds.slice(0, 10)
|
||
|
|
const relationships: Array<{from: string, to: string}> = []
|
||
|
|
|
||
|
|
for (let i = 0; i < subgraph.length; i++) {
|
||
|
|
for (let j = i + 1; j < subgraph.length; j++) {
|
||
|
|
relationships.push({
|
||
|
|
from: subgraph[i],
|
||
|
|
to: subgraph[j]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
const results = await Promise.all(
|
||
|
|
relationships.map(r =>
|
||
|
|
brainy.relate({ from: r.from, to: r.to, type: 'relatedTo' })
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
metricsCollector.recordBatch(
|
||
|
|
relationships.length,
|
||
|
|
results,
|
||
|
|
duration,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
expect(relationships.length).toBe(45) // 10 choose 2
|
||
|
|
expect(results.every(r => typeof r === 'string')).toBe(true) // relate returns ID
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Mixed Batch Operations', () => {
|
||
|
|
it('should handle mixed operation types concurrently', async () => {
|
||
|
|
// Prepare different operation types
|
||
|
|
const addOps = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Mixed add ${i}`,
|
||
|
|
type: 'document' as const
|
||
|
|
}))
|
||
|
|
|
||
|
|
// Add initial entities for update/delete
|
||
|
|
const setupResult = await brainy.addMany({
|
||
|
|
items: Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Setup ${i}`,
|
||
|
|
type: 'document' as const
|
||
|
|
}))
|
||
|
|
})
|
||
|
|
|
||
|
|
const updateIds = setupResult.successful.slice(0, 50)
|
||
|
|
const deleteIds = setupResult.successful.slice(50, 100)
|
||
|
|
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Execute all operations concurrently
|
||
|
|
const [addResults, updateResults, deleteResults] = await Promise.all([
|
||
|
|
brainy.addMany({ items: addOps }),
|
||
|
|
Promise.all(updateIds.map(id =>
|
||
|
|
brainy.update({ id, metadata: { updated: true } })
|
||
|
|
)),
|
||
|
|
Promise.all(deleteIds.map(id => brainy.delete(id)))
|
||
|
|
])
|
||
|
|
|
||
|
|
const duration = performance.now() - startTime
|
||
|
|
|
||
|
|
console.log(`Mixed operations completed in ${duration.toFixed(2)}ms`)
|
||
|
|
|
||
|
|
expect(addResults.successful.length).toBe(100)
|
||
|
|
expect(updateResults.length).toBe(50)
|
||
|
|
// Delete returns void, so just check length
|
||
|
|
expect(deleteResults).toHaveLength(50)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Error Recovery', () => {
|
||
|
|
it('should recover from transient failures', async () => {
|
||
|
|
let attemptCount = 0
|
||
|
|
const entities = Array(10).fill(null).map((_, i) => ({
|
||
|
|
data: `Retry test ${i}`,
|
||
|
|
type: 'document' as const
|
||
|
|
}))
|
||
|
|
|
||
|
|
// Simulate transient failures
|
||
|
|
const originalAdd = brainy.add.bind(brainy)
|
||
|
|
brainy.add = async function(params: any) {
|
||
|
|
attemptCount++
|
||
|
|
if (attemptCount % 3 === 0) {
|
||
|
|
throw new Error('Transient failure')
|
||
|
|
}
|
||
|
|
return originalAdd(params)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results: any[] = []
|
||
|
|
for (const entity of entities) {
|
||
|
|
let retries = 0
|
||
|
|
while (retries < 3) {
|
||
|
|
try {
|
||
|
|
const id = await brainy.add(entity)
|
||
|
|
results.push({ status: 'fulfilled', value: id })
|
||
|
|
break
|
||
|
|
} catch (error) {
|
||
|
|
retries++
|
||
|
|
if (retries === 3) {
|
||
|
|
results.push({ status: 'rejected', reason: error })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const successful = results.filter(r => r.status === 'fulfilled')
|
||
|
|
expect(successful.length).toBeGreaterThanOrEqual(6) // Most should succeed with retry
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle memory pressure gracefully', async () => {
|
||
|
|
const largeEntities = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: 'x'.repeat(100000), // 100KB each = 10MB total
|
||
|
|
type: 'document' as const,
|
||
|
|
metadata: { index: i }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const startMemory = process.memoryUsage().heapUsed
|
||
|
|
|
||
|
|
// Process with memory monitoring
|
||
|
|
const batchSize = 10
|
||
|
|
const results: any[] = []
|
||
|
|
|
||
|
|
for (let i = 0; i < largeEntities.length; i += batchSize) {
|
||
|
|
const batch = largeEntities.slice(i, i + batchSize)
|
||
|
|
|
||
|
|
// Check memory before processing
|
||
|
|
const currentMemory = process.memoryUsage().heapUsed
|
||
|
|
const memoryUsed = currentMemory - startMemory
|
||
|
|
|
||
|
|
if (memoryUsed > 100 * 1024 * 1024) { // If > 100MB
|
||
|
|
if (global.gc) global.gc() // Force GC if available
|
||
|
|
}
|
||
|
|
|
||
|
|
const batchResult = await brainy.addMany({ items: batch })
|
||
|
|
results.push(...batchResult.successful)
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(results.length).toBe(100)
|
||
|
|
|
||
|
|
const finalMemory = process.memoryUsage().heapUsed - startMemory
|
||
|
|
expect(finalMemory).toBeLessThan(200 * 1024 * 1024) // Should stay under 200MB
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Performance Report', () => {
|
||
|
|
it('should generate comprehensive batch operations report', async () => {
|
||
|
|
metricsCollector.generateReport()
|
||
|
|
|
||
|
|
const metrics = metricsCollector.getMetrics()
|
||
|
|
expect(metrics.length).toBeGreaterThan(0)
|
||
|
|
|
||
|
|
// Validate performance targets
|
||
|
|
for (const metric of metrics) {
|
||
|
|
expect(metric.successCount).toBeGreaterThan(0)
|
||
|
|
if (metric.operation.includes('100')) {
|
||
|
|
expect(metric.throughput).toBeGreaterThan(50) // At least 50 items/sec
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|