539 lines
20 KiB
JavaScript
539 lines
20 KiB
JavaScript
const request = require('supertest');
|
|
const app = require('../server');
|
|
const database = require('../models/database');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const XLSX = require('xlsx');
|
|
|
|
describe('Performance Tests - Large Datasets', () => {
|
|
let testDbPath;
|
|
|
|
beforeAll(async () => {
|
|
// Set up test database with performance optimizations
|
|
testDbPath = path.join(__dirname, '..', 'test_performance.db');
|
|
if (fs.existsSync(testDbPath)) {
|
|
fs.unlinkSync(testDbPath);
|
|
}
|
|
database.dbPath = testDbPath;
|
|
await database.initialize();
|
|
|
|
// Apply additional performance settings for testing
|
|
const db = database.getDatabase();
|
|
db.pragma('cache_size = 2000'); // Increase cache size for tests
|
|
db.pragma('temp_store = memory');
|
|
db.pragma('mmap_size = 536870912'); // 512MB
|
|
});
|
|
|
|
afterAll(async () => {
|
|
database.close();
|
|
if (fs.existsSync(testDbPath)) {
|
|
fs.unlinkSync(testDbPath);
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clean up database before each test
|
|
const db = database.getDatabase();
|
|
db.exec('DELETE FROM inventory_history');
|
|
db.exec('DELETE FROM inventory');
|
|
db.exec('DELETE FROM products');
|
|
db.exec('DELETE FROM import_sessions');
|
|
});
|
|
|
|
describe('Large Dataset Import Performance', () => {
|
|
test('should import 1000+ products within acceptable time', async () => {
|
|
const productCount = 1000;
|
|
const testData = [['Product Code', 'Description', 'Quantity', 'Category', 'Unit of Measure']];
|
|
|
|
// Generate large dataset
|
|
console.log(`Generating ${productCount} test products...`);
|
|
for (let i = 1; i <= productCount; i++) {
|
|
testData.push([
|
|
`PERF${i.toString().padStart(6, '0')}`,
|
|
`Performance Test Product ${i} - ${Math.random().toString(36).substring(7)}`,
|
|
Math.floor(Math.random() * 1000) + 1,
|
|
['Electronics', 'Books', 'Clothing', 'Home', 'Sports'][i % 5],
|
|
['pcs', 'kg', 'lbs', 'units', 'boxes'][i % 5]
|
|
]);
|
|
}
|
|
|
|
const worksheet = XLSX.utils.aoa_to_sheet(testData);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'LargeDataset');
|
|
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
|
|
console.log(`Starting import of ${productCount} products...`);
|
|
const startTime = Date.now();
|
|
|
|
const response = await request(app)
|
|
.post('/api/products/import/excel')
|
|
.attach('file', buffer, 'large-dataset.xlsx')
|
|
.timeout(30000) // 30 second timeout
|
|
.expect(200);
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`Import completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`);
|
|
console.log(`Average: ${(duration/productCount).toFixed(2)}ms per product`);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.importResults.imported).toBe(productCount);
|
|
expect(duration).toBeLessThan(15000); // Should complete within 15 seconds
|
|
expect(duration / productCount).toBeLessThan(15); // Less than 15ms per product
|
|
});
|
|
|
|
test('should handle 5000+ products with memory efficiency', async () => {
|
|
const productCount = 5000;
|
|
console.log(`Testing memory efficiency with ${productCount} products...`);
|
|
|
|
const initialMemory = process.memoryUsage();
|
|
console.log('Initial memory usage:', {
|
|
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`,
|
|
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`
|
|
});
|
|
|
|
// Generate large dataset in chunks to test streaming
|
|
const chunkSize = 1000;
|
|
let totalImported = 0;
|
|
|
|
for (let chunk = 0; chunk < Math.ceil(productCount / chunkSize); chunk++) {
|
|
const chunkStart = chunk * chunkSize + 1;
|
|
const chunkEnd = Math.min((chunk + 1) * chunkSize, productCount);
|
|
const chunkData = [['Product Code', 'Description', 'Quantity', 'Category']];
|
|
|
|
for (let i = chunkStart; i <= chunkEnd; i++) {
|
|
chunkData.push([
|
|
`CHUNK${chunk}_${i.toString().padStart(6, '0')}`,
|
|
`Chunk ${chunk} Product ${i}`,
|
|
Math.floor(Math.random() * 100) + 1,
|
|
`Category${i % 10}`
|
|
]);
|
|
}
|
|
|
|
const worksheet = XLSX.utils.aoa_to_sheet(chunkData);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, `Chunk${chunk}`);
|
|
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
|
|
const chunkStartTime = Date.now();
|
|
const response = await request(app)
|
|
.post('/api/products/import/excel')
|
|
.attach('file', buffer, `chunk-${chunk}.xlsx`)
|
|
.timeout(30000)
|
|
.expect(200);
|
|
|
|
const chunkDuration = Date.now() - chunkStartTime;
|
|
totalImported += response.body.data.importResults.imported;
|
|
|
|
console.log(`Chunk ${chunk + 1}/${Math.ceil(productCount / chunkSize)} completed in ${chunkDuration}ms`);
|
|
|
|
// Check memory usage after each chunk
|
|
const currentMemory = process.memoryUsage();
|
|
const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
|
|
console.log(`Memory increase: ${memoryIncrease.toFixed(2)}MB`);
|
|
|
|
// Memory should not increase excessively (less than 100MB per 1000 products)
|
|
expect(memoryIncrease).toBeLessThan(100 * (chunk + 1));
|
|
}
|
|
|
|
expect(totalImported).toBe(productCount);
|
|
console.log(`Total imported: ${totalImported} products`);
|
|
|
|
// Verify final memory usage is reasonable
|
|
const finalMemory = process.memoryUsage();
|
|
const totalMemoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
|
|
console.log(`Total memory increase: ${totalMemoryIncrease.toFixed(2)}MB`);
|
|
expect(totalMemoryIncrease).toBeLessThan(500); // Less than 500MB total increase
|
|
});
|
|
});
|
|
|
|
describe('Database Query Performance', () => {
|
|
beforeEach(async () => {
|
|
// Create a large dataset for query testing
|
|
const db = database.getDatabase();
|
|
const insertProduct = db.prepare(`
|
|
INSERT INTO products (product_code, description, category, unit_of_measure)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
const insertInventory = db.prepare(`
|
|
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
|
|
console.log('Setting up large dataset for query performance tests...');
|
|
const transaction = db.transaction(() => {
|
|
for (let i = 1; i <= 2000; i++) {
|
|
const result = insertProduct.run(
|
|
`QUERY${i.toString().padStart(6, '0')}`,
|
|
`Query Test Product ${i}`,
|
|
`Category${i % 20}`,
|
|
'pcs'
|
|
);
|
|
insertInventory.run(
|
|
result.lastInsertRowid,
|
|
Math.floor(Math.random() * 1000) + 1,
|
|
10,
|
|
500
|
|
);
|
|
}
|
|
});
|
|
transaction();
|
|
console.log('Large dataset setup completed');
|
|
});
|
|
|
|
test('should perform fast product lookups with indexes', async () => {
|
|
const iterations = 100;
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const productCode = `QUERY${Math.floor(Math.random() * 2000 + 1).toString().padStart(6, '0')}`;
|
|
const response = await request(app)
|
|
.get(`/api/products/barcode/${productCode}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const avgTime = duration / iterations;
|
|
|
|
console.log(`${iterations} product lookups completed in ${duration}ms`);
|
|
console.log(`Average lookup time: ${avgTime.toFixed(2)}ms`);
|
|
|
|
expect(avgTime).toBeLessThan(50); // Less than 50ms per lookup
|
|
});
|
|
|
|
test('should perform fast inventory queries with pagination', async () => {
|
|
const pageSize = 50;
|
|
const totalPages = 10;
|
|
const startTime = Date.now();
|
|
|
|
for (let page = 0; page < totalPages; page++) {
|
|
const response = await request(app)
|
|
.get(`/api/inventory?limit=${pageSize}&offset=${page * pageSize}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.length).toBeLessThanOrEqual(pageSize);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const avgTime = duration / totalPages;
|
|
|
|
console.log(`${totalPages} paginated queries completed in ${duration}ms`);
|
|
console.log(`Average query time: ${avgTime.toFixed(2)}ms`);
|
|
|
|
expect(avgTime).toBeLessThan(100); // Less than 100ms per paginated query
|
|
});
|
|
|
|
test('should perform fast filtered searches', async () => {
|
|
const categories = ['Category0', 'Category5', 'Category10', 'Category15'];
|
|
const startTime = Date.now();
|
|
|
|
for (const category of categories) {
|
|
const response = await request(app)
|
|
.get(`/api/inventory?category=${category}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
|
|
// Verify all results match the filter
|
|
response.body.data.forEach(item => {
|
|
expect(item.category).toBe(category);
|
|
});
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const avgTime = duration / categories.length;
|
|
|
|
console.log(`${categories.length} filtered searches completed in ${duration}ms`);
|
|
console.log(`Average search time: ${avgTime.toFixed(2)}ms`);
|
|
|
|
expect(avgTime).toBeLessThan(200); // Less than 200ms per filtered search
|
|
});
|
|
|
|
test('should handle complex aggregation queries efficiently', async () => {
|
|
const startTime = Date.now();
|
|
|
|
// Test low stock query
|
|
const lowStockResponse = await request(app)
|
|
.get('/api/inventory/low-stock')
|
|
.expect(200);
|
|
|
|
// Test inventory summary with grouping
|
|
const summaryResponse = await request(app)
|
|
.get('/api/inventory?groupBy=category')
|
|
.expect(200);
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
console.log(`Complex aggregation queries completed in ${duration}ms`);
|
|
|
|
expect(duration).toBeLessThan(1000); // Less than 1 second for complex queries
|
|
expect(lowStockResponse.body.success).toBe(true);
|
|
expect(summaryResponse.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Concurrent Operations Performance', () => {
|
|
beforeEach(async () => {
|
|
// Create products for concurrent testing
|
|
const db = database.getDatabase();
|
|
const insertProduct = db.prepare(`
|
|
INSERT INTO products (product_code, description, category)
|
|
VALUES (?, ?, ?)
|
|
`);
|
|
const insertInventory = db.prepare(`
|
|
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
|
|
const transaction = db.transaction(() => {
|
|
for (let i = 1; i <= 100; i++) {
|
|
const result = insertProduct.run(
|
|
`CONCURRENT${i.toString().padStart(3, '0')}`,
|
|
`Concurrent Test Product ${i}`,
|
|
'Test'
|
|
);
|
|
insertInventory.run(result.lastInsertRowid, 100, 10, 200);
|
|
}
|
|
});
|
|
transaction();
|
|
});
|
|
|
|
test('should handle concurrent inventory updates efficiently', async () => {
|
|
const concurrentUsers = 20;
|
|
const updatesPerUser = 5;
|
|
|
|
console.log(`Testing ${concurrentUsers} concurrent users with ${updatesPerUser} updates each...`);
|
|
|
|
const startTime = Date.now();
|
|
const promises = [];
|
|
|
|
for (let user = 0; user < concurrentUsers; user++) {
|
|
for (let update = 0; update < updatesPerUser; update++) {
|
|
const productId = Math.floor(Math.random() * 100) + 1;
|
|
const newLevel = Math.floor(Math.random() * 200) + 1;
|
|
|
|
const promise = request(app)
|
|
.put(`/api/inventory/product/${productId}/level`)
|
|
.send({
|
|
newLevel: newLevel,
|
|
changeReason: `Concurrent update by user ${user}`,
|
|
updatedBy: `user-${user}`
|
|
});
|
|
|
|
promises.push(promise);
|
|
}
|
|
}
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const duration = Date.now() - startTime;
|
|
|
|
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length;
|
|
const conflicts = results.filter(r => r.status === 'fulfilled' && r.value.status === 409).length;
|
|
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 500)).length;
|
|
|
|
console.log(`Concurrent updates completed in ${duration}ms`);
|
|
console.log(`Successful: ${successful}, Conflicts: ${conflicts}, Errors: ${errors}`);
|
|
console.log(`Average time per update: ${(duration / promises.length).toFixed(2)}ms`);
|
|
|
|
expect(successful + conflicts).toBe(promises.length); // All should either succeed or conflict
|
|
expect(errors).toBe(0); // No server errors
|
|
expect(duration).toBeLessThan(10000); // Complete within 10 seconds
|
|
expect(successful).toBeGreaterThan(promises.length * 0.7); // At least 70% success rate
|
|
});
|
|
|
|
test('should handle concurrent read operations efficiently', async () => {
|
|
const concurrentReads = 50;
|
|
|
|
console.log(`Testing ${concurrentReads} concurrent read operations...`);
|
|
|
|
const startTime = Date.now();
|
|
const promises = [];
|
|
|
|
for (let i = 0; i < concurrentReads; i++) {
|
|
const operations = [
|
|
request(app).get('/api/products'),
|
|
request(app).get('/api/inventory'),
|
|
request(app).get('/api/inventory/low-stock'),
|
|
request(app).get(`/api/products/${Math.floor(Math.random() * 100) + 1}`),
|
|
request(app).get(`/api/inventory/product/${Math.floor(Math.random() * 100) + 1}`)
|
|
];
|
|
|
|
promises.push(...operations);
|
|
}
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const duration = Date.now() - startTime;
|
|
|
|
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length;
|
|
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 400)).length;
|
|
|
|
console.log(`${promises.length} concurrent reads completed in ${duration}ms`);
|
|
console.log(`Successful: ${successful}, Errors: ${errors}`);
|
|
console.log(`Average time per read: ${(duration / promises.length).toFixed(2)}ms`);
|
|
|
|
expect(successful).toBe(promises.length); // All reads should succeed
|
|
expect(errors).toBe(0);
|
|
expect(duration).toBeLessThan(5000); // Complete within 5 seconds
|
|
expect(duration / promises.length).toBeLessThan(100); // Less than 100ms per read
|
|
});
|
|
});
|
|
|
|
describe('Export Performance', () => {
|
|
beforeEach(async () => {
|
|
// Create large dataset for export testing
|
|
const db = database.getDatabase();
|
|
const insertProduct = db.prepare(`
|
|
INSERT INTO products (product_code, description, category, unit_of_measure)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
const insertInventory = db.prepare(`
|
|
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level, last_updated, updated_by)
|
|
VALUES (?, ?, ?, ?, datetime('now'), ?)
|
|
`);
|
|
|
|
console.log('Setting up large dataset for export performance tests...');
|
|
const transaction = db.transaction(() => {
|
|
for (let i = 1; i <= 1500; i++) {
|
|
const result = insertProduct.run(
|
|
`EXPORT${i.toString().padStart(6, '0')}`,
|
|
`Export Test Product ${i} - ${Math.random().toString(36).substring(7)}`,
|
|
`Category${i % 15}`,
|
|
['pcs', 'kg', 'lbs', 'units'][i % 4]
|
|
);
|
|
insertInventory.run(
|
|
result.lastInsertRowid,
|
|
Math.floor(Math.random() * 500) + 1,
|
|
Math.floor(Math.random() * 20) + 5,
|
|
Math.floor(Math.random() * 200) + 300,
|
|
`export-user-${i % 10}`
|
|
);
|
|
}
|
|
});
|
|
transaction();
|
|
console.log('Export dataset setup completed');
|
|
});
|
|
|
|
test('should export large datasets efficiently', async () => {
|
|
console.log('Testing large dataset export performance...');
|
|
|
|
const startTime = Date.now();
|
|
const response = await request(app)
|
|
.get('/api/codes/export/excel')
|
|
.timeout(15000) // 15 second timeout
|
|
.expect(200);
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
console.log(`Export of 1500 products completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`);
|
|
|
|
expect(response.headers['content-type']).toContain('application/vnd.openxmlformats');
|
|
expect(duration).toBeLessThan(10000); // Should complete within 10 seconds
|
|
|
|
// Verify export content
|
|
const workbook = XLSX.read(response.body, { type: 'buffer' });
|
|
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
const data = XLSX.utils.sheet_to_json(sheet);
|
|
|
|
expect(data.length).toBe(1500);
|
|
expect(data[0]).toHaveProperty('Product Code');
|
|
expect(data[0]).toHaveProperty('Description');
|
|
expect(data[0]).toHaveProperty('Current Level');
|
|
});
|
|
|
|
test('should handle filtered exports efficiently', async () => {
|
|
const filters = [
|
|
{ category: 'Category0' },
|
|
{ category: 'Category5' },
|
|
{ lowStock: 'true' },
|
|
{ category: 'Category10', minLevel: 50 }
|
|
];
|
|
|
|
for (const filter of filters) {
|
|
const queryString = new URLSearchParams(filter).toString();
|
|
|
|
const startTime = Date.now();
|
|
const response = await request(app)
|
|
.get(`/api/codes/export/excel?${queryString}`)
|
|
.timeout(10000)
|
|
.expect(200);
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
console.log(`Filtered export (${queryString}) completed in ${duration}ms`);
|
|
|
|
expect(duration).toBeLessThan(5000); // Filtered exports should be faster
|
|
expect(response.headers['content-type']).toContain('application/vnd.openxmlformats');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Memory and Resource Management', () => {
|
|
test('should maintain stable memory usage during sustained operations', async () => {
|
|
const initialMemory = process.memoryUsage();
|
|
console.log('Initial memory:', {
|
|
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`,
|
|
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`
|
|
});
|
|
|
|
// Perform sustained operations
|
|
for (let cycle = 0; cycle < 10; cycle++) {
|
|
// Create some products
|
|
const products = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
const response = await request(app)
|
|
.post('/api/products')
|
|
.send({
|
|
name: `MEMORY${cycle}_${i}`,
|
|
description: `Memory Test Product ${cycle}-${i}`,
|
|
category: 'MemoryTest'
|
|
})
|
|
.expect(201);
|
|
products.push(response.body.data.id);
|
|
}
|
|
|
|
// Update inventory levels
|
|
for (const productId of products) {
|
|
await request(app)
|
|
.post(`/api/inventory/product/${productId}`)
|
|
.send({
|
|
initialLevel: Math.floor(Math.random() * 100) + 1,
|
|
minimumLevel: 5,
|
|
maximumLevel: 150,
|
|
updatedBy: 'memory-test'
|
|
})
|
|
.expect(201);
|
|
}
|
|
|
|
// Clean up products to test garbage collection
|
|
for (const productId of products) {
|
|
await request(app)
|
|
.delete(`/api/products/${productId}`)
|
|
.expect(200);
|
|
}
|
|
|
|
// Check memory usage
|
|
const currentMemory = process.memoryUsage();
|
|
const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
|
|
|
|
console.log(`Cycle ${cycle + 1}/10 - Memory increase: ${memoryIncrease.toFixed(2)}MB`);
|
|
|
|
// Memory should not continuously increase
|
|
expect(memoryIncrease).toBeLessThan(50 * (cycle + 1)); // Allow some growth but not excessive
|
|
}
|
|
|
|
// Force garbage collection if available
|
|
if (global.gc) {
|
|
global.gc();
|
|
}
|
|
|
|
const finalMemory = process.memoryUsage();
|
|
const totalIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
|
|
|
|
console.log('Final memory increase:', `${totalIncrease.toFixed(2)}MB`);
|
|
expect(totalIncrease).toBeLessThan(100); // Total increase should be reasonable
|
|
});
|
|
});
|
|
}); |