delta-sync
Version:
A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.
498 lines (497 loc) • 22.4 kB
JavaScript
// tester/PerformanceTester.ts
export class PerformanceTester {
constructor(adapter, options = {}) {
this.createdFileIds = [];
this.createdItemIds = [];
this.adapter = adapter;
this.options = {
testStoreName: options.testStoreName || 'perf_test',
itemCount: options.itemCount || 100,
iterations: options.iterations || 3,
fileSize: options.fileSize || 1024 * 10, // 10KB
concurrentOperations: options.concurrentOperations || 10,
verbose: options.verbose !== undefined ? options.verbose : true,
cleanupAfterTest: options.cleanupAfterTest !== undefined ? options.cleanupAfterTest : true
};
this.testStoreName = this.options.testStoreName;
}
/**
* Run all performance tests
*/
async runAllTests() {
console.log('=== Starting adapter performance tests ===');
const results = {};
try {
// Basic CRUD performance
results.singleItemWrite = await this.testSingleItemWrite();
results.singleItemRead = await this.testSingleItemRead();
results.singleItemDelete = await this.testSingleItemDelete();
// Bulk operations performance
results.bulkWrite = await this.testBulkWrite();
results.bulkRead = await this.testBulkRead();
results.bulkDelete = await this.testBulkDelete();
// Bulk file operations performance
results.bulkFileWrite = await this.testBulkFileWrite();
results.bulkFileRead = await this.testBulkFileRead();
results.bulkFileDelete = await this.testBulkFileDelete();
// Pagination performance
results.pagination = await this.testPagination();
// Stress test
results.stressTest = await this.testStress();
console.log('=== Adapter performance tests completed ===');
// Calculate overall score (lower is better)
const totalTimeMs = Object.values(results).reduce((sum, result) => sum + result.averageTimeMs, 0);
const averageTimeMs = totalTimeMs / Object.keys(results).length;
console.log(`Overall average operation time: ${averageTimeMs.toFixed(2)}ms`);
if (this.options.verbose) {
console.log('\nDetailed results:');
Object.entries(results).forEach(([testName, result]) => {
console.log(`${testName}:`);
console.log(` Average time: ${result.averageTimeMs.toFixed(2)}ms`);
console.log(` Min time: ${result.minTimeMs.toFixed(2)}ms`);
console.log(` Max time: ${result.maxTimeMs.toFixed(2)}ms`);
if (result.operationsPerSecond) {
console.log(` Operations/sec: ${result.operationsPerSecond.toFixed(2)}`);
}
if (result.throughputMBps) {
console.log(` Throughput: ${result.throughputMBps.toFixed(2)}MB/s`);
}
});
}
// Clean up all test data if configured
if (this.options.cleanupAfterTest) {
await this.cleanupTestData();
}
return {
success: true,
results
};
}
catch (error) {
console.error('Uncaught error during performance testing:', error);
// Try to clean up data even if test fails
if (this.options.cleanupAfterTest) {
try {
await this.cleanupTestData();
}
catch (cleanupError) {
console.error('Error cleaning up test data:', cleanupError);
}
}
return {
success: false,
results
};
}
}
// Test single item write performance
async testSingleItemWrite() {
console.log('Testing single item write performance...');
const times = [];
for (let i = 0; i < this.options.iterations; i++) {
const itemId = `perf_single_write_${Date.now()}_${i}`;
const item = {
id: itemId,
value: `Test value ${i}`,
};
const startTime = performance.now();
await this.adapter.putBulk(this.testStoreName, [item]);
const endTime = performance.now();
this.createdItemIds.push(itemId);
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, 1);
}
// Test single item read performance
async testSingleItemRead() {
console.log('Testing single item read performance...');
const times = [];
// Create test item first
const testItemId = `perf_single_read_${Date.now()}`;
const testItem = {
id: testItemId,
value: 'Test read value',
};
await this.adapter.putBulk(this.testStoreName, [testItem]);
this.createdItemIds.push(testItemId);
for (let i = 0; i < this.options.iterations; i++) {
const startTime = performance.now();
await this.adapter.readBulk(this.testStoreName, [testItemId]);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, 1);
}
// Test single item delete performance
async testSingleItemDelete() {
console.log('Testing single item delete performance...');
const times = [];
for (let i = 0; i < this.options.iterations; i++) {
// Create item to delete
const itemId = `perf_single_delete_${Date.now()}_${i}`;
const item = {
id: itemId,
value: `Test delete value ${i}`,
};
await this.adapter.putBulk(this.testStoreName, [item]);
const startTime = performance.now();
await this.adapter.deleteBulk(this.testStoreName, [itemId]);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, 1);
}
// Test bulk write performance
async testBulkWrite() {
console.log('Testing bulk write performance...');
const times = [];
for (let iter = 0; iter < this.options.iterations; iter++) {
const items = [];
for (let i = 0; i < this.options.itemCount; i++) {
const itemId = `perf_bulk_write_${Date.now()}_${iter}_${i}`;
items.push({
id: itemId,
value: `Bulk write test value ${i}`,
});
this.createdItemIds.push(itemId);
}
const startTime = performance.now();
await this.adapter.putBulk(this.testStoreName, items);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount);
}
// Test bulk read performance
async testBulkRead() {
console.log('Testing bulk read performance...');
const times = [];
// Create test items first
const testItems = [];
const testItemIds = [];
for (let i = 0; i < this.options.itemCount; i++) {
const itemId = `perf_bulk_read_${Date.now()}_${i}`;
testItems.push({
id: itemId,
value: `Bulk read test value ${i}`,
});
testItemIds.push(itemId);
this.createdItemIds.push(itemId);
}
await this.adapter.putBulk(this.testStoreName, testItems);
for (let i = 0; i < this.options.iterations; i++) {
const startTime = performance.now();
await this.adapter.readBulk(this.testStoreName, testItemIds);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount);
}
// Test bulk delete performance
async testBulkDelete() {
console.log('Testing bulk delete performance...');
const times = [];
for (let iter = 0; iter < this.options.iterations; iter++) {
// Create items to delete
const items = [];
const itemIds = [];
for (let i = 0; i < this.options.itemCount; i++) {
const itemId = `perf_bulk_delete_${Date.now()}_${iter}_${i}`;
items.push({
id: itemId,
value: `Bulk delete test value ${i}`,
});
itemIds.push(itemId);
}
await this.adapter.putBulk(this.testStoreName, items);
const startTime = performance.now();
await this.adapter.deleteBulk(this.testStoreName, itemIds);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount);
}
// Test bulk file write performance
async testBulkFileWrite() {
console.log('Testing bulk file write performance...');
const times = [];
for (let iter = 0; iter < this.options.iterations; iter++) {
const files = [];
// Create multiple test files
for (let i = 0; i < this.options.itemCount; i++) {
// Create test file of specified size
const testData = new Uint8Array(this.options.fileSize);
// Fill with random data
for (let j = 0; j < testData.length; j++) {
testData[j] = Math.floor(Math.random() * 256);
}
const testBlob = new Blob([testData], { type: 'application/octet-stream' });
const fileId = `perf_bulk_file_write_${Date.now()}_${iter}_${i}.bin`;
files.push({
content: testBlob,
fileId: fileId
});
}
const startTime = performance.now();
const attachments = await this.adapter.saveFiles(files);
const endTime = performance.now();
// Save actual file IDs for later cleanup
for (const attachment of attachments) {
if (attachment && attachment.id) {
this.createdFileIds.push(attachment.id);
}
}
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount, this.options.fileSize * this.options.itemCount);
}
// Test bulk file read performance
async testBulkFileRead() {
console.log('Testing bulk file read performance...');
const times = [];
// Create test files
const files = [];
const fileIds = [];
for (let i = 0; i < this.options.itemCount; i++) {
const testData = new Uint8Array(this.options.fileSize);
// Fill with random data
for (let j = 0; j < testData.length; j++) {
testData[j] = Math.floor(Math.random() * 256);
}
const testBlob = new Blob([testData], { type: 'application/octet-stream' });
const fileId = `perf_bulk_file_read_${Date.now()}_${i}.bin`;
files.push({
content: testBlob,
fileId: fileId
});
fileIds.push(fileId);
}
const attachments = await this.adapter.saveFiles(files);
// Save actual file IDs for cleanup and testing
const actualFileIds = [];
for (let i = 0; i < attachments.length; i++) {
const actualFileId = attachments[i]?.id || fileIds[i];
actualFileIds.push(actualFileId);
this.createdFileIds.push(actualFileId);
}
for (let i = 0; i < this.options.iterations; i++) {
const startTime = performance.now();
await this.adapter.readFiles(actualFileIds);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount, this.options.fileSize * this.options.itemCount);
}
// Test bulk file delete performance
async testBulkFileDelete() {
console.log('Testing bulk file delete performance...');
const times = [];
for (let iter = 0; iter < this.options.iterations; iter++) {
// Create files to delete
const files = [];
const fileIds = [];
for (let i = 0; i < this.options.itemCount; i++) {
const testData = new Uint8Array(this.options.fileSize);
const testBlob = new Blob([testData], { type: 'application/octet-stream' });
const fileId = `perf_bulk_file_delete_${Date.now()}_${iter}_${i}.bin`;
files.push({
content: testBlob,
fileId: fileId
});
fileIds.push(fileId);
}
const attachments = await this.adapter.saveFiles(files);
// Use actual file IDs for deletion test
const actualFileIds = attachments.map(att => att.id);
const startTime = performance.now();
await this.adapter.deleteFiles(actualFileIds);
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.itemCount);
}
// Test pagination performance
async testPagination() {
console.log('Testing pagination performance...');
const times = [];
// Create data for pagination testing
const items = [];
for (let i = 0; i < this.options.itemCount; i++) {
const itemId = `perf_pagination_${Date.now()}_${i}`;
items.push({
id: itemId,
value: `Pagination test value ${i}`,
});
this.createdItemIds.push(itemId);
}
await this.adapter.putBulk(this.testStoreName, items);
// Test different page sizes
const pageSizes = [10, 25, 50];
for (const pageSize of pageSizes) {
for (let i = 0; i < this.options.iterations; i++) {
const startTime = performance.now();
await this.adapter.readByVersion(this.testStoreName, {
limit: pageSize,
offset: i * pageSize % this.options.itemCount
});
const endTime = performance.now();
times.push(endTime - startTime);
}
}
return this.calculatePerformanceResult(times, pageSizes[1]); // Use medium page size for ops/sec
}
// Test stress handling and concurrent operations
async testStress() {
console.log('Running concurrent operations stress test...');
const times = [];
for (let iter = 0; iter < this.options.iterations; iter++) {
const startTime = performance.now();
// Mix different operations
const operations = [];
const stressItemIds = [];
// Reduce concurrent ops to make test more reliable
const reducedConcurrentOps = Math.min(this.options.concurrentOperations, 5);
for (let i = 0; i < reducedConcurrentOps; i++) {
const opType = i % 4; // 0 = write, 1 = read, 2 = delete, 3 = file op
if (opType === 0) {
// Write operation
const itemId = `stress_write_${Date.now()}_${i}`;
const item = {
id: itemId,
value: `Stress test write ${i}`,
};
stressItemIds.push(itemId);
this.createdItemIds.push(itemId);
operations.push(this.adapter.putBulk(this.testStoreName, [item]));
}
else if (opType === 1) {
// Read operation (using pagination to avoid needing specific ID)
operations.push(this.adapter.readByVersion(this.testStoreName, {
limit: 5,
offset: i * 5 % 20
}));
}
else if (opType === 2) {
// Delete operation - create then delete
const itemId = `stress_delete_${Date.now()}_${i}`;
const item = {
id: itemId,
value: `Stress test delete ${i}`,
};
// Ensure creation is complete before deletion to avoid race condition
await this.adapter.putBulk(this.testStoreName, [item]);
operations.push(this.adapter.deleteBulk(this.testStoreName, [itemId]));
}
else {
// File operation - handle file save and read separately to avoid concurrent ops on same file
const testData = new Uint8Array(512); // Reduce file size to 512 bytes
const testBlob = new Blob([testData], { type: 'application/octet-stream' });
const fileId = `stress_file_${Date.now()}_${i}.bin`;
try {
// Upload file first
const attachments = await this.adapter.saveFiles([{ content: testBlob, fileId }]);
if (attachments[0]) {
this.createdFileIds.push(attachments[0].id);
// Then try to read
operations.push(this.adapter.readFiles([attachments[0].id]));
}
}
catch (error) {
console.error(`File operation failed (${fileId}):`, error);
// Continue test, don't interrupt
}
}
// Add small delay after each operation to avoid race conditions
await new Promise(resolve => setTimeout(resolve, 20));
}
// Execute operations sequentially, not concurrently
for (const operation of operations) {
try {
await operation;
}
catch (error) {
console.warn('Stress test operation failed:', error);
// Continue test, don't interrupt
}
}
const endTime = performance.now();
times.push(endTime - startTime);
}
return this.calculatePerformanceResult(times, this.options.concurrentOperations);
}
// Clean up test data
async cleanupTestData() {
console.log('Cleaning up test data...');
try {
// Clean up created items
if (this.createdItemIds.length > 0) {
// Process in batches to avoid deleting too many at once
const batchSize = 100;
for (let i = 0; i < this.createdItemIds.length; i += batchSize) {
const batch = this.createdItemIds.slice(i, i + batchSize);
await this.adapter.deleteBulk(this.testStoreName, batch);
}
console.log(`Deleted ${this.createdItemIds.length} test records`);
}
// Clean up created files
if (this.createdFileIds.length > 0) {
// Process file deletion in batches
const batchSize = 50;
for (let i = 0; i < this.createdFileIds.length; i += batchSize) {
const batch = this.createdFileIds.slice(i, i + batchSize);
await this.adapter.deleteFiles(batch);
}
console.log(`Deleted ${this.createdFileIds.length} test files`);
}
// Find and delete other possible test data
try {
const result = await this.adapter.readByVersion(this.testStoreName, { limit: 1000 });
if (result && result.items.length > 0) {
// Find items that look like test data
const testItems = result.items.filter(item => item.id && (item.id.includes('perf_') ||
item.id.includes('test_') ||
item.id.includes('bulk_') ||
item.id.includes('stress_')));
if (testItems.length > 0) {
// Delete discovered test items
await this.adapter.deleteBulk(this.testStoreName, testItems.map(item => item.id));
console.log(`Deleted ${testItems.length} additional test records by scanning`);
}
}
}
catch (error) {
console.warn('Scan cleanup of test records failed:', error);
}
console.log('Cleanup complete');
}
catch (error) {
console.error('Error during cleanup:', error);
throw error;
}
}
// Calculate performance metrics from raw timing data
calculatePerformanceResult(times, operationsPerTest, dataSize) {
const sumTime = times.reduce((sum, time) => sum + time, 0);
const avgTime = sumTime / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const opsPerSecond = operationsPerTest * (1000 / avgTime);
let throughput;
if (dataSize) {
// Calculate MB/s
throughput = (dataSize / 1024 / 1024) / (avgTime / 1000);
}
return {
averageTimeMs: avgTime,
minTimeMs: minTime,
maxTimeMs: maxTime,
operationsPerSecond: opsPerSecond,
throughputMBps: throughput
};
}
}
export async function testAdapterPerformance(adapter, options) {
const tester = new PerformanceTester(adapter, options);
const result = await tester.runAllTests();
return result.results;
}