433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import { BrainyData } from '../src/index.js'
|
||
|
|
import { BatchProcessingAugmentation } from '../src/augmentations/batchProcessingAugmentation.js'
|
||
|
|
|
||
|
|
describe('Batch Processing Augmentation', () => {
|
||
|
|
let db: BrainyData | null = null
|
||
|
|
|
||
|
|
// Helper to create test vectors
|
||
|
|
const createTestVector = (seed: number = 0) => {
|
||
|
|
return new Array(384).fill(0).map((_, i) => Math.sin(i + seed) * 0.5)
|
||
|
|
}
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
if (db) {
|
||
|
|
await db.cleanup?.()
|
||
|
|
db = null
|
||
|
|
}
|
||
|
|
|
||
|
|
// Force garbage collection if available
|
||
|
|
if (global.gc) {
|
||
|
|
global.gc()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Configuration and Initialization', () => {
|
||
|
|
it('should initialize with default configuration', async () => {
|
||
|
|
db = new BrainyData()
|
||
|
|
await db.init()
|
||
|
|
|
||
|
|
// Batch processing should be enabled by default
|
||
|
|
// Test by adding many items quickly
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
promises.push(
|
||
|
|
db.add(createTestVector(i), { id: `test${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(10)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should accept custom batch configuration', async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 50,
|
||
|
|
batchWaitTime: 10 // 10ms wait time
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
|
||
|
|
// Should handle custom batch size
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 100; i++) {
|
||
|
|
promises.push(
|
||
|
|
db.add(createTestVector(i), { id: `batch${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(100)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Batching Behavior', () => {
|
||
|
|
beforeEach(async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 10,
|
||
|
|
batchWaitTime: 50 // 50ms wait
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should batch operations within wait time', async () => {
|
||
|
|
const startTime = performance.now()
|
||
|
|
|
||
|
|
// Add items quickly (should be batched)
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `quick${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
await Promise.all(promises)
|
||
|
|
const elapsed = performance.now() - startTime
|
||
|
|
|
||
|
|
// Should complete quickly due to batching
|
||
|
|
expect(elapsed).toBeLessThan(200) // Much less than 10 * individual operation time
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should flush batch when size limit reached', async () => {
|
||
|
|
// Add exactly batch size items
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `size${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should flush immediately when batch is full
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(10)
|
||
|
|
|
||
|
|
// Verify all were added
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
const item = await db!.get(`size${i}`)
|
||
|
|
expect(item).toBeDefined()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should flush batch after wait time expires', async () => {
|
||
|
|
// Add fewer items than batch size
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `timer${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should flush after wait time even if batch not full
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(5)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Adaptive Batching', () => {
|
||
|
|
it('should adapt batch size based on performance', async () => {
|
||
|
|
const batch = new BatchProcessingAugmentation({
|
||
|
|
maxBatchSize: 100,
|
||
|
|
enableAdaptiveBatching: true,
|
||
|
|
adaptiveThreshold: 50 // 50ms target
|
||
|
|
})
|
||
|
|
|
||
|
|
// Initialize with mock context
|
||
|
|
await batch.initialize({
|
||
|
|
brain: {},
|
||
|
|
storage: {},
|
||
|
|
config: {},
|
||
|
|
log: () => {}
|
||
|
|
} as any)
|
||
|
|
|
||
|
|
// Simulate operations with varying performance
|
||
|
|
// (In real usage, the augmentation would measure actual operation time)
|
||
|
|
|
||
|
|
const stats = batch.getStats()
|
||
|
|
expect(stats).toBeDefined()
|
||
|
|
expect(stats.totalBatches).toBe(0)
|
||
|
|
expect(stats.adaptiveAdjustments).toBe(0)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should increase batch size for fast operations', async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 10,
|
||
|
|
batchWaitTime: 20,
|
||
|
|
enableAdaptiveBatching: true
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
|
||
|
|
// Add many items (simulating fast operations)
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 100; i++) {
|
||
|
|
promises.push(
|
||
|
|
db.add(createTestVector(i), { id: `adaptive${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
await Promise.all(promises)
|
||
|
|
|
||
|
|
// Batch size should have adapted (implementation dependent)
|
||
|
|
// Just verify operations completed
|
||
|
|
expect(promises.length).toBe(100)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Performance Impact', () => {
|
||
|
|
it('should improve throughput for bulk operations', async () => {
|
||
|
|
// Test without batching
|
||
|
|
const dbNoBatch = new BrainyData({
|
||
|
|
batchSize: 1, // Effectively no batching
|
||
|
|
batchWaitTime: 0
|
||
|
|
})
|
||
|
|
await dbNoBatch.init()
|
||
|
|
|
||
|
|
const startNoBatch = performance.now()
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
await dbNoBatch.add(createTestVector(i), { id: `nobatch${i}` })
|
||
|
|
}
|
||
|
|
const timeNoBatch = performance.now() - startNoBatch
|
||
|
|
|
||
|
|
await dbNoBatch.cleanup?.()
|
||
|
|
|
||
|
|
// Test with batching
|
||
|
|
const dbWithBatch = new BrainyData({
|
||
|
|
batchSize: 25,
|
||
|
|
batchWaitTime: 10
|
||
|
|
})
|
||
|
|
await dbWithBatch.init()
|
||
|
|
|
||
|
|
const startBatch = performance.now()
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
promises.push(
|
||
|
|
dbWithBatch.add(createTestVector(i), { id: `batch${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
await Promise.all(promises)
|
||
|
|
const timeBatch = performance.now() - startBatch
|
||
|
|
|
||
|
|
await dbWithBatch.cleanup?.()
|
||
|
|
|
||
|
|
// Batched should be faster for bulk operations
|
||
|
|
expect(timeBatch).toBeLessThan(timeNoBatch)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should not delay single operations significantly', async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 10,
|
||
|
|
batchWaitTime: 100 // 100ms wait
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
|
||
|
|
// Single operation
|
||
|
|
const start = performance.now()
|
||
|
|
await db.add(createTestVector(1), { id: 'single' })
|
||
|
|
const elapsed = performance.now() - start
|
||
|
|
|
||
|
|
// Should not wait full batch time for single item
|
||
|
|
expect(elapsed).toBeLessThan(150) // Some overhead is OK
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Operation Types', () => {
|
||
|
|
beforeEach(async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 5,
|
||
|
|
batchWaitTime: 20
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should batch add operations', async () => {
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `add${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(5)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should batch addNoun operations', async () => {
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.addNoun(createTestVector(i), {
|
||
|
|
id: `noun${i}`,
|
||
|
|
data: `Noun ${i}`
|
||
|
|
})
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(5)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should batch mixed operations', async () => {
|
||
|
|
// Mix different operation types
|
||
|
|
const promises = []
|
||
|
|
|
||
|
|
// Add some nouns
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.addNoun(createTestVector(i), { id: `mixed${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add some regular items
|
||
|
|
for (let i = 3; i < 5; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `mixed${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await Promise.all(promises)
|
||
|
|
expect(results.length).toBe(5)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Error Handling', () => {
|
||
|
|
beforeEach(async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 5,
|
||
|
|
batchWaitTime: 20
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle errors in batch operations', async () => {
|
||
|
|
// Mix valid and invalid operations
|
||
|
|
const promises = []
|
||
|
|
|
||
|
|
// Valid operations
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(i), { id: `valid${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invalid operation (duplicate ID)
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(0), { id: 'valid0' })
|
||
|
|
)
|
||
|
|
|
||
|
|
// More valid operations
|
||
|
|
promises.push(
|
||
|
|
db!.add(createTestVector(4), { id: 'valid4' })
|
||
|
|
)
|
||
|
|
|
||
|
|
// Should not fail entire batch
|
||
|
|
const results = await Promise.allSettled(promises)
|
||
|
|
|
||
|
|
const fulfilled = results.filter(r => r.status === 'fulfilled')
|
||
|
|
expect(fulfilled.length).toBeGreaterThanOrEqual(4)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle batch timeout gracefully', async () => {
|
||
|
|
// Create batch with very short timeout
|
||
|
|
const quickBatch = new BatchProcessingAugmentation({
|
||
|
|
maxBatchSize: 10,
|
||
|
|
maxWaitTime: 1 // 1ms timeout
|
||
|
|
})
|
||
|
|
|
||
|
|
await quickBatch.initialize({
|
||
|
|
brain: {},
|
||
|
|
storage: {},
|
||
|
|
config: {},
|
||
|
|
log: () => {}
|
||
|
|
} as any)
|
||
|
|
|
||
|
|
// Should handle timeout without crashing
|
||
|
|
await quickBatch.shutdown()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Statistics and Monitoring', () => {
|
||
|
|
it('should track batch statistics', async () => {
|
||
|
|
const batch = new BatchProcessingAugmentation({
|
||
|
|
maxBatchSize: 10,
|
||
|
|
maxWaitTime: 50
|
||
|
|
})
|
||
|
|
|
||
|
|
await batch.initialize({
|
||
|
|
brain: {},
|
||
|
|
storage: {},
|
||
|
|
config: {},
|
||
|
|
log: () => {}
|
||
|
|
} as any)
|
||
|
|
|
||
|
|
const stats = batch.getStats()
|
||
|
|
expect(stats).toBeDefined()
|
||
|
|
expect(stats).toHaveProperty('totalBatches')
|
||
|
|
expect(stats).toHaveProperty('totalOperations')
|
||
|
|
expect(stats).toHaveProperty('averageBatchSize')
|
||
|
|
expect(stats).toHaveProperty('averageWaitTime')
|
||
|
|
expect(stats).toHaveProperty('currentBatchSize')
|
||
|
|
expect(stats).toHaveProperty('adaptiveAdjustments')
|
||
|
|
|
||
|
|
await batch.shutdown()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should export batch metrics', async () => {
|
||
|
|
db = new BrainyData({
|
||
|
|
batchSize: 5,
|
||
|
|
batchWaitTime: 10
|
||
|
|
})
|
||
|
|
await db.init()
|
||
|
|
|
||
|
|
// Perform some operations
|
||
|
|
const promises = []
|
||
|
|
for (let i = 0; i < 20; i++) {
|
||
|
|
promises.push(
|
||
|
|
db.add(createTestVector(i), { id: `metric${i}` })
|
||
|
|
)
|
||
|
|
}
|
||
|
|
await Promise.all(promises)
|
||
|
|
|
||
|
|
// Get augmentation stats (if exposed through BrainyData)
|
||
|
|
// This would need API support in BrainyData
|
||
|
|
// For now, just verify operations completed
|
||
|
|
expect(promises.length).toBe(20)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Standalone Usage', () => {
|
||
|
|
it('should work as standalone augmentation', () => {
|
||
|
|
const batch = new BatchProcessingAugmentation({
|
||
|
|
maxBatchSize: 100,
|
||
|
|
maxWaitTime: 50
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(batch.name).toBe('BatchProcessing')
|
||
|
|
expect(batch.timing).toBe('around')
|
||
|
|
expect(batch.priority).toBe(80) // High priority
|
||
|
|
expect(batch.operations).toContain('add')
|
||
|
|
expect(batch.operations).toContain('addNoun')
|
||
|
|
expect(batch.operations).toContain('saveNoun')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle lifecycle correctly', async () => {
|
||
|
|
const batch = new BatchProcessingAugmentation()
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
await batch.initialize({
|
||
|
|
brain: {},
|
||
|
|
storage: {},
|
||
|
|
config: {},
|
||
|
|
log: () => {}
|
||
|
|
} as any)
|
||
|
|
|
||
|
|
// Should be ready
|
||
|
|
const stats = batch.getStats()
|
||
|
|
expect(stats.totalBatches).toBe(0)
|
||
|
|
|
||
|
|
// Shutdown
|
||
|
|
await batch.shutdown()
|
||
|
|
|
||
|
|
// Should flush any pending batches on shutdown
|
||
|
|
const finalStats = batch.getStats()
|
||
|
|
expect(finalStats.totalBatches).toBeGreaterThanOrEqual(0)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|