/**
* Performance tests for indexing throughput
* Measures items/second for mocked indexing operations
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
createEmbeddingMock,
BATCH_SIZE,
EMBEDDING_DIM
} from '../helpers/indexing-mocks.js'
import {
generateTestEmails,
generateTestMessages,
generateCalendarEvents,
generateSearchTexts
} from '../helpers/test-data-generators.js'
import {
measureTime,
calculateThroughput,
ThroughputTracker,
assertThroughput
} from '../helpers/performance-utils.js'
describe('Indexing Throughput', () => {
let mockEmbedder
beforeEach(() => {
vi.clearAllMocks()
const mock = createEmbeddingMock()
mockEmbedder = mock.mockEmbedder
})
describe('email processing throughput', () => {
it('should process emails at >= 100 items/sec (mocked)', async () => {
const count = 100
const emails = generateTestEmails(count)
const { duration } = await measureTime(async () => {
// Simulate email processing: parse + embed
const searchTexts = emails.map(e => e.content.substring(0, 500))
for (let i = 0; i < searchTexts.length; i += BATCH_SIZE) {
const batch = searchTexts.slice(i, i + BATCH_SIZE)
await mockEmbedder(batch, { pooling: 'mean', normalize: true })
}
})
const throughput = calculateThroughput(count, duration)
console.log(`Email throughput: ${throughput.toFixed(1)} items/sec`)
expect(throughput).toBeGreaterThanOrEqual(100)
})
it('should maintain throughput with larger batches', async () => {
const count = 320 // 10 batches of 32
const { duration } = await measureTime(async () => {
const texts = generateSearchTexts(count)
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE)
await mockEmbedder(batch, { pooling: 'mean', normalize: true })
}
})
const throughput = calculateThroughput(count, duration)
expect(throughput).toBeGreaterThanOrEqual(100)
})
})
describe('message processing throughput', () => {
it('should process messages at >= 200 items/sec (mocked)', async () => {
const count = 200
const messages = generateTestMessages(count)
const { duration } = await measureTime(async () => {
const searchTexts = messages.map(m => `From: ${m.sender}\nMessage: ${m.text}`.substring(0, 500))
for (let i = 0; i < searchTexts.length; i += BATCH_SIZE) {
const batch = searchTexts.slice(i, i + BATCH_SIZE)
await mockEmbedder(batch, { pooling: 'mean', normalize: true })
}
})
const throughput = calculateThroughput(count, duration)
console.log(`Message throughput: ${throughput.toFixed(1)} items/sec`)
expect(throughput).toBeGreaterThanOrEqual(200)
})
})
describe('calendar processing throughput', () => {
it('should process calendar events at >= 150 items/sec (mocked)', async () => {
const count = 150
const events = generateCalendarEvents(count)
const { duration } = await measureTime(async () => {
const searchTexts = events.map(e =>
`Event: ${e.title}\nCalendar: ${e.calendar}\nLocation: ${e.location}`.substring(0, 500)
)
for (let i = 0; i < searchTexts.length; i += BATCH_SIZE) {
const batch = searchTexts.slice(i, i + BATCH_SIZE)
await mockEmbedder(batch, { pooling: 'mean', normalize: true })
}
})
const throughput = calculateThroughput(count, duration)
console.log(`Calendar throughput: ${throughput.toFixed(1)} items/sec`)
expect(throughput).toBeGreaterThanOrEqual(150)
})
})
describe('full indexAll throughput', () => {
it('should complete indexAll within 10 seconds for 500 items (mocked)', async () => {
const emailCount = 200
const messageCount = 200
const calendarCount = 100
const totalCount = emailCount + messageCount + calendarCount
const emails = generateTestEmails(emailCount)
const messages = generateTestMessages(messageCount)
const events = generateCalendarEvents(calendarCount)
const { duration } = await measureTime(async () => {
// Simulate indexAll: process all three sources
// Emails
const emailTexts = emails.map(e => e.content.substring(0, 500))
for (let i = 0; i < emailTexts.length; i += BATCH_SIZE) {
await mockEmbedder(emailTexts.slice(i, i + BATCH_SIZE), { pooling: 'mean', normalize: true })
}
// Messages
const msgTexts = messages.map(m => m.text.substring(0, 500))
for (let i = 0; i < msgTexts.length; i += BATCH_SIZE) {
await mockEmbedder(msgTexts.slice(i, i + BATCH_SIZE), { pooling: 'mean', normalize: true })
}
// Calendar
const eventTexts = events.map(e => e.title)
for (let i = 0; i < eventTexts.length; i += BATCH_SIZE) {
await mockEmbedder(eventTexts.slice(i, i + BATCH_SIZE), { pooling: 'mean', normalize: true })
}
})
console.log(`Full indexAll (${totalCount} items): ${duration.toFixed(0)}ms`)
expect(duration).toBeLessThan(10000) // < 10 seconds
})
})
})
describe('Throughput Tracker', () => {
it('should track batch throughput correctly', async () => {
const tracker = new ThroughputTracker()
tracker.start()
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 10))
tracker.recordBatch(32)
}
const summary = tracker.getSummary()
expect(summary.totalItems).toBe(160)
expect(summary.batchCount).toBe(5)
expect(summary.overallThroughput).toBeGreaterThan(0)
})
it('should calculate batch-level statistics', async () => {
const tracker = new ThroughputTracker()
tracker.start()
tracker.recordBatch(32)
await new Promise(r => setTimeout(r, 50))
tracker.recordBatch(32)
await new Promise(r => setTimeout(r, 50))
tracker.recordBatch(32)
const summary = tracker.getSummary()
expect(summary.avgBatchThroughput).toBeGreaterThan(0)
expect(summary.minBatchThroughput).toBeLessThanOrEqual(summary.avgBatchThroughput)
expect(summary.maxBatchThroughput).toBeGreaterThanOrEqual(summary.avgBatchThroughput)
})
})
describe('Throughput under load', () => {
it('should maintain consistent throughput over multiple batches', async () => {
const mockEmbedder = createEmbeddingMock().mockEmbedder
const batchThroughputs = []
// Warmup batch - not measured (eliminates V8 JIT cold-start overhead)
const warmupTexts = generateSearchTexts(BATCH_SIZE)
await mockEmbedder(warmupTexts, { pooling: 'mean', normalize: true })
for (let batch = 0; batch < 10; batch++) {
const texts = generateSearchTexts(BATCH_SIZE)
const start = performance.now()
await mockEmbedder(texts, { pooling: 'mean', normalize: true })
const duration = performance.now() - start
batchThroughputs.push(calculateThroughput(BATCH_SIZE, duration))
}
const avgThroughput = batchThroughputs.reduce((a, b) => a + b, 0) / batchThroughputs.length
const minThroughput = Math.min(...batchThroughputs)
console.log(`Average throughput: ${avgThroughput.toFixed(0)} items/sec`)
console.log(`Min throughput: ${minThroughput.toFixed(0)} items/sec`)
// Min should be at least 50% of average (achievable after warmup)
expect(minThroughput).toBeGreaterThan(avgThroughput * 0.5)
})
})