Initial commit: Inventory Barcode System
This commit is contained in:
539
__tests__/performance.test.js
Normal file
539
__tests__/performance.test.js
Normal file
@ -0,0 +1,539 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user