- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
560 lines
21 KiB
PHP
Executable File
560 lines
21 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Parametric BOM Performance Testing Script
|
|
*
|
|
* This script performs performance testing of the parametric BOM system
|
|
* including load testing, memory usage analysis, and scalability testing.
|
|
*
|
|
* Usage: php scripts/validation/performance_test.php
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../bootstrap/app.php';
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\Design\DesignModel;
|
|
use App\Services\Design\BomResolverService;
|
|
use App\Services\Design\ModelParameterService;
|
|
use App\Services\Design\ModelFormulaService;
|
|
use App\Services\Design\BomConditionRuleService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class BomPerformanceTester
|
|
{
|
|
private Tenant $tenant;
|
|
private DesignModel $model;
|
|
private BomResolverService $bomResolver;
|
|
private array $performanceResults = [];
|
|
private int $passedTests = 0;
|
|
private int $totalTests = 0;
|
|
|
|
// Performance thresholds (adjustable based on requirements)
|
|
private const THRESHOLDS = [
|
|
'single_resolution_ms' => 200, // Single BOM resolution should be under 200ms
|
|
'batch_10_resolution_ms' => 1500, // 10 resolutions should be under 1.5s
|
|
'batch_100_resolution_ms' => 12000, // 100 resolutions should be under 12s
|
|
'memory_usage_mb' => 50, // Memory usage should stay under 50MB
|
|
'db_queries_per_resolution' => 20, // Should not exceed 20 DB queries per resolution
|
|
'concurrent_resolution_ms' => 500 // Concurrent resolutions should complete under 500ms
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->output("\n=== Parametric BOM Performance Testing ===");
|
|
$this->output("Testing system performance and scalability...\n");
|
|
|
|
$this->setupServices();
|
|
}
|
|
|
|
private function setupServices(): void
|
|
{
|
|
// Find test tenant and model
|
|
$this->tenant = Tenant::where('code', 'KSS_DEMO')->first();
|
|
if (!$this->tenant) {
|
|
throw new Exception('KSS_DEMO tenant not found. Please run KSS01ModelSeeder first.');
|
|
}
|
|
|
|
$this->model = DesignModel::where('tenant_id', $this->tenant->id)
|
|
->where('code', 'KSS01')
|
|
->first();
|
|
if (!$this->model) {
|
|
throw new Exception('KSS01 model not found. Please run KSS01ModelSeeder first.');
|
|
}
|
|
|
|
// Setup BOM resolver with query logging
|
|
$this->bomResolver = new BomResolverService(
|
|
new ModelParameterService(),
|
|
new ModelFormulaService(),
|
|
new BomConditionRuleService()
|
|
);
|
|
$this->bomResolver->setTenantId($this->tenant->id);
|
|
$this->bomResolver->setApiUserId(1);
|
|
}
|
|
|
|
public function run(): void
|
|
{
|
|
try {
|
|
// Enable query logging for database performance analysis
|
|
DB::enableQueryLog();
|
|
|
|
// Test single resolution performance
|
|
$this->testSingleResolutionPerformance();
|
|
|
|
// Test batch resolution performance
|
|
$this->testBatchResolutionPerformance();
|
|
|
|
// Test memory usage
|
|
$this->testMemoryUsage();
|
|
|
|
// Test database query efficiency
|
|
$this->testDatabaseQueryEfficiency();
|
|
|
|
// Test different parameter combinations
|
|
$this->testParameterVariationPerformance();
|
|
|
|
// Test concurrent resolution (simulated)
|
|
$this->testConcurrentResolution();
|
|
|
|
// Test large dataset performance
|
|
$this->testLargeDatasetPerformance();
|
|
|
|
// Generate performance report
|
|
$this->generatePerformanceReport();
|
|
|
|
} catch (Exception $e) {
|
|
$this->output("\n❌ Critical Error: " . $e->getMessage());
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
private function testSingleResolutionPerformance(): void
|
|
{
|
|
$this->output("\n🏁 Testing Single Resolution Performance...");
|
|
|
|
$testParams = [
|
|
'W0' => 1200,
|
|
'H0' => 800,
|
|
'screen_type' => 'FABRIC',
|
|
'install_type' => 'WALL'
|
|
];
|
|
|
|
// Warm-up run
|
|
$this->bomResolver->resolveBom($this->model->id, $testParams);
|
|
|
|
// Performance test runs
|
|
$times = [];
|
|
$runs = 10;
|
|
|
|
for ($i = 0; $i < $runs; $i++) {
|
|
$startTime = microtime(true);
|
|
$result = $this->bomResolver->resolveBom($this->model->id, $testParams);
|
|
$endTime = microtime(true);
|
|
|
|
$times[] = ($endTime - $startTime) * 1000;
|
|
}
|
|
|
|
$avgTime = array_sum($times) / count($times);
|
|
$minTime = min($times);
|
|
$maxTime = max($times);
|
|
|
|
$this->performanceResults['single_resolution'] = [
|
|
'avg_time_ms' => $avgTime,
|
|
'min_time_ms' => $minTime,
|
|
'max_time_ms' => $maxTime,
|
|
'runs' => $runs
|
|
];
|
|
|
|
$passed = $avgTime < self::THRESHOLDS['single_resolution_ms'];
|
|
$this->recordTest('Single Resolution Performance', $passed);
|
|
|
|
$this->output(sprintf(" Avg: %.2fms, Min: %.2fms, Max: %.2fms (Target: <%.0fms)",
|
|
$avgTime, $minTime, $maxTime, self::THRESHOLDS['single_resolution_ms']));
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Single resolution performance within threshold");
|
|
} else {
|
|
$this->output(" ❌ Single resolution performance exceeds threshold");
|
|
}
|
|
}
|
|
|
|
private function testBatchResolutionPerformance(): void
|
|
{
|
|
$this->output("\n📈 Testing Batch Resolution Performance...");
|
|
|
|
$scenarios = $this->generateTestScenarios(10);
|
|
|
|
// Test batch of 10
|
|
$startTime = microtime(true);
|
|
foreach ($scenarios as $params) {
|
|
$this->bomResolver->resolveBom($this->model->id, $params);
|
|
}
|
|
$batch10Time = (microtime(true) - $startTime) * 1000;
|
|
|
|
$this->performanceResults['batch_10_resolution'] = [
|
|
'total_time_ms' => $batch10Time,
|
|
'avg_per_resolution_ms' => $batch10Time / 10
|
|
];
|
|
|
|
$passed10 = $batch10Time < self::THRESHOLDS['batch_10_resolution_ms'];
|
|
$this->recordTest('Batch 10 Resolution Performance', $passed10);
|
|
|
|
$this->output(sprintf(" Batch 10: %.2fms total, %.2fms avg (Target: <%.0fms total)",
|
|
$batch10Time, $batch10Time / 10, self::THRESHOLDS['batch_10_resolution_ms']));
|
|
|
|
// Test batch of 100
|
|
$largeBatch = $this->generateTestScenarios(100);
|
|
|
|
$startTime = microtime(true);
|
|
foreach ($largeBatch as $params) {
|
|
$this->bomResolver->resolveBom($this->model->id, $params);
|
|
}
|
|
$batch100Time = (microtime(true) - $startTime) * 1000;
|
|
|
|
$this->performanceResults['batch_100_resolution'] = [
|
|
'total_time_ms' => $batch100Time,
|
|
'avg_per_resolution_ms' => $batch100Time / 100
|
|
];
|
|
|
|
$passed100 = $batch100Time < self::THRESHOLDS['batch_100_resolution_ms'];
|
|
$this->recordTest('Batch 100 Resolution Performance', $passed100);
|
|
|
|
$this->output(sprintf(" Batch 100: %.2fms total, %.2fms avg (Target: <%.0fms total)",
|
|
$batch100Time, $batch100Time / 100, self::THRESHOLDS['batch_100_resolution_ms']));
|
|
}
|
|
|
|
private function testMemoryUsage(): void
|
|
{
|
|
$this->output("\n💾 Testing Memory Usage...");
|
|
|
|
$initialMemory = memory_get_usage(true);
|
|
$peakMemory = memory_get_peak_usage(true);
|
|
|
|
// Perform multiple resolutions and monitor memory
|
|
$scenarios = $this->generateTestScenarios(50);
|
|
|
|
foreach ($scenarios as $params) {
|
|
$this->bomResolver->resolveBom($this->model->id, $params);
|
|
}
|
|
|
|
$finalMemory = memory_get_usage(true);
|
|
$finalPeakMemory = memory_get_peak_usage(true);
|
|
|
|
$memoryUsed = ($finalMemory - $initialMemory) / 1024 / 1024;
|
|
$peakMemoryUsed = ($finalPeakMemory - $peakMemory) / 1024 / 1024;
|
|
|
|
$this->performanceResults['memory_usage'] = [
|
|
'memory_used_mb' => $memoryUsed,
|
|
'peak_memory_used_mb' => $peakMemoryUsed,
|
|
'initial_memory_mb' => $initialMemory / 1024 / 1024,
|
|
'final_memory_mb' => $finalMemory / 1024 / 1024
|
|
];
|
|
|
|
$passed = $peakMemoryUsed < self::THRESHOLDS['memory_usage_mb'];
|
|
$this->recordTest('Memory Usage', $passed);
|
|
|
|
$this->output(sprintf(" Memory used: %.2fMB, Peak: %.2fMB (Target: <%.0fMB)",
|
|
$memoryUsed, $peakMemoryUsed, self::THRESHOLDS['memory_usage_mb']));
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Memory usage within acceptable limits");
|
|
} else {
|
|
$this->output(" ❌ Memory usage exceeds threshold");
|
|
}
|
|
}
|
|
|
|
private function testDatabaseQueryEfficiency(): void
|
|
{
|
|
$this->output("\n📊 Testing Database Query Efficiency...");
|
|
|
|
// Clear query log
|
|
DB::flushQueryLog();
|
|
|
|
$testParams = [
|
|
'W0' => 1500,
|
|
'H0' => 1000,
|
|
'screen_type' => 'STEEL',
|
|
'install_type' => 'CEILING'
|
|
];
|
|
|
|
// Perform resolution and count queries
|
|
$this->bomResolver->resolveBom($this->model->id, $testParams);
|
|
|
|
$queries = DB::getQueryLog();
|
|
$queryCount = count($queries);
|
|
|
|
// Analyze query types
|
|
$selectQueries = 0;
|
|
$totalQueryTime = 0;
|
|
|
|
foreach ($queries as $query) {
|
|
if (stripos($query['query'], 'select') === 0) {
|
|
$selectQueries++;
|
|
}
|
|
$totalQueryTime += $query['time'];
|
|
}
|
|
|
|
$this->performanceResults['database_efficiency'] = [
|
|
'total_queries' => $queryCount,
|
|
'select_queries' => $selectQueries,
|
|
'total_query_time_ms' => $totalQueryTime,
|
|
'avg_query_time_ms' => $queryCount > 0 ? $totalQueryTime / $queryCount : 0
|
|
];
|
|
|
|
$passed = $queryCount <= self::THRESHOLDS['db_queries_per_resolution'];
|
|
$this->recordTest('Database Query Efficiency', $passed);
|
|
|
|
$this->output(sprintf(" Total queries: %d, Select queries: %d, Total time: %.2fms (Target: <%d queries)",
|
|
$queryCount, $selectQueries, $totalQueryTime, self::THRESHOLDS['db_queries_per_resolution']));
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Database query count within threshold");
|
|
} else {
|
|
$this->output(" ❌ Too many database queries per resolution");
|
|
}
|
|
|
|
// Show some sample queries for analysis
|
|
if (count($queries) > 0) {
|
|
$this->output(" Sample queries:");
|
|
$sampleCount = min(3, count($queries));
|
|
for ($i = 0; $i < $sampleCount; $i++) {
|
|
$query = $queries[$i];
|
|
$shortQuery = substr($query['query'], 0, 80) . (strlen($query['query']) > 80 ? '...' : '');
|
|
$this->output(sprintf(" %s (%.2fms)", $shortQuery, $query['time']));
|
|
}
|
|
}
|
|
}
|
|
|
|
private function testParameterVariationPerformance(): void
|
|
{
|
|
$this->output("\n🔄 Testing Parameter Variation Performance...");
|
|
|
|
$variations = [
|
|
'Small' => ['W0' => 600, 'H0' => 400, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'],
|
|
'Medium' => ['W0' => 1200, 'H0' => 800, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'],
|
|
'Large' => ['W0' => 2400, 'H0' => 1800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING'],
|
|
'Extra Large' => ['W0' => 3000, 'H0' => 2500, 'screen_type' => 'STEEL', 'install_type' => 'RECESSED']
|
|
];
|
|
|
|
$variationResults = [];
|
|
|
|
foreach ($variations as $name => $params) {
|
|
$times = [];
|
|
$runs = 5;
|
|
|
|
for ($i = 0; $i < $runs; $i++) {
|
|
$startTime = microtime(true);
|
|
$result = $this->bomResolver->resolveBom($this->model->id, $params);
|
|
$endTime = microtime(true);
|
|
|
|
$times[] = ($endTime - $startTime) * 1000;
|
|
}
|
|
|
|
$avgTime = array_sum($times) / count($times);
|
|
$bomItemCount = count($result['resolved_bom']);
|
|
|
|
$variationResults[$name] = [
|
|
'avg_time_ms' => $avgTime,
|
|
'bom_items' => $bomItemCount,
|
|
'area' => $result['calculated_values']['area']
|
|
];
|
|
|
|
$this->output(sprintf(" %s: %.2fms, %d BOM items, %.2fm²",
|
|
$name, $avgTime, $bomItemCount, $result['calculated_values']['area']));
|
|
}
|
|
|
|
$this->performanceResults['parameter_variations'] = $variationResults;
|
|
|
|
// Check if performance is consistent across variations
|
|
$times = array_column($variationResults, 'avg_time_ms');
|
|
$maxVariation = max($times) - min($times);
|
|
$avgVariation = array_sum($times) / count($times);
|
|
|
|
$passed = $maxVariation < ($avgVariation * 2); // Variation should not exceed 200% of average
|
|
$this->recordTest('Parameter Variation Performance Consistency', $passed);
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Performance consistent across parameter variations");
|
|
} else {
|
|
$this->output(" ❌ Performance varies significantly with different parameters");
|
|
}
|
|
}
|
|
|
|
private function testConcurrentResolution(): void
|
|
{
|
|
$this->output("\n🚀 Testing Concurrent Resolution (Simulated)...");
|
|
|
|
// Simulate concurrent requests by rapidly executing multiple resolutions
|
|
$concurrentCount = 5;
|
|
$scenarios = $this->generateTestScenarios($concurrentCount);
|
|
|
|
$startTime = microtime(true);
|
|
|
|
// Simulate concurrent processing
|
|
$results = [];
|
|
foreach ($scenarios as $i => $params) {
|
|
$resolveStart = microtime(true);
|
|
$result = $this->bomResolver->resolveBom($this->model->id, $params);
|
|
$resolveEnd = microtime(true);
|
|
|
|
$results[] = [
|
|
'index' => $i,
|
|
'time_ms' => ($resolveEnd - $resolveStart) * 1000,
|
|
'bom_items' => count($result['resolved_bom'])
|
|
];
|
|
}
|
|
|
|
$totalTime = (microtime(true) - $startTime) * 1000;
|
|
$avgConcurrentTime = array_sum(array_column($results, 'time_ms')) / count($results);
|
|
|
|
$this->performanceResults['concurrent_resolution'] = [
|
|
'concurrent_count' => $concurrentCount,
|
|
'total_time_ms' => $totalTime,
|
|
'avg_resolution_time_ms' => $avgConcurrentTime,
|
|
'results' => $results
|
|
];
|
|
|
|
$passed = $avgConcurrentTime < self::THRESHOLDS['concurrent_resolution_ms'];
|
|
$this->recordTest('Concurrent Resolution Performance', $passed);
|
|
|
|
$this->output(sprintf(" %d concurrent resolutions: %.2fms total, %.2fms avg (Target: <%.0fms avg)",
|
|
$concurrentCount, $totalTime, $avgConcurrentTime, self::THRESHOLDS['concurrent_resolution_ms']));
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Concurrent resolution performance acceptable");
|
|
} else {
|
|
$this->output(" ❌ Concurrent resolution performance degraded");
|
|
}
|
|
}
|
|
|
|
private function testLargeDatasetPerformance(): void
|
|
{
|
|
$this->output("\n📊 Testing Large Dataset Performance...");
|
|
|
|
// Test with large number of variations
|
|
$largeDataset = $this->generateTestScenarios(200);
|
|
|
|
$batchSize = 20;
|
|
$batchTimes = [];
|
|
|
|
for ($i = 0; $i < count($largeDataset); $i += $batchSize) {
|
|
$batch = array_slice($largeDataset, $i, $batchSize);
|
|
|
|
$batchStart = microtime(true);
|
|
foreach ($batch as $params) {
|
|
$this->bomResolver->resolveBom($this->model->id, $params);
|
|
}
|
|
$batchEnd = microtime(true);
|
|
|
|
$batchTimes[] = ($batchEnd - $batchStart) * 1000;
|
|
}
|
|
|
|
$totalTime = array_sum($batchTimes);
|
|
$avgBatchTime = $totalTime / count($batchTimes);
|
|
$throughput = count($largeDataset) / ($totalTime / 1000); // resolutions per second
|
|
|
|
$this->performanceResults['large_dataset'] = [
|
|
'total_resolutions' => count($largeDataset),
|
|
'batch_size' => $batchSize,
|
|
'total_time_ms' => $totalTime,
|
|
'avg_batch_time_ms' => $avgBatchTime,
|
|
'throughput_per_second' => $throughput
|
|
];
|
|
|
|
$passed = $throughput >= 10; // At least 10 resolutions per second
|
|
$this->recordTest('Large Dataset Throughput', $passed);
|
|
|
|
$this->output(sprintf(" %d resolutions: %.2fms total, %.2f resolutions/sec (Target: >=10/sec)",
|
|
count($largeDataset), $totalTime, $throughput));
|
|
|
|
if ($passed) {
|
|
$this->output(" ✅ Large dataset throughput acceptable");
|
|
} else {
|
|
$this->output(" ❌ Large dataset throughput too low");
|
|
}
|
|
}
|
|
|
|
private function generateTestScenarios(int $count): array
|
|
{
|
|
$scenarios = [];
|
|
$widths = [600, 800, 1000, 1200, 1500, 1800, 2000, 2400, 3000];
|
|
$heights = [400, 600, 800, 1000, 1200, 1500, 1800, 2000, 2500];
|
|
$screenTypes = ['FABRIC', 'STEEL'];
|
|
$installTypes = ['WALL', 'CEILING', 'RECESSED'];
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$scenarios[] = [
|
|
'W0' => $widths[array_rand($widths)],
|
|
'H0' => $heights[array_rand($heights)],
|
|
'screen_type' => $screenTypes[array_rand($screenTypes)],
|
|
'install_type' => $installTypes[array_rand($installTypes)]
|
|
];
|
|
}
|
|
|
|
return $scenarios;
|
|
}
|
|
|
|
private function recordTest(string $name, bool $passed): void
|
|
{
|
|
$this->totalTests++;
|
|
if ($passed) {
|
|
$this->passedTests++;
|
|
}
|
|
}
|
|
|
|
private function output(string $message): void
|
|
{
|
|
echo $message . "\n";
|
|
Log::info("BOM Performance: " . $message);
|
|
}
|
|
|
|
private function generatePerformanceReport(): void
|
|
{
|
|
$this->output("\n" . str_repeat("=", 70));
|
|
$this->output("PARAMETRIC BOM PERFORMANCE REPORT");
|
|
$this->output(str_repeat("=", 70));
|
|
|
|
$successRate = $this->totalTests > 0 ? round(($this->passedTests / $this->totalTests) * 100, 1) : 0;
|
|
|
|
$this->output("Total Performance Tests: {$this->totalTests}");
|
|
$this->output("Passed: {$this->passedTests}");
|
|
$this->output("Failed: " . ($this->totalTests - $this->passedTests));
|
|
$this->output("Success Rate: {$successRate}%");
|
|
|
|
$this->output("\n📊 PERFORMANCE METRICS:");
|
|
|
|
// Single resolution performance
|
|
if (isset($this->performanceResults['single_resolution'])) {
|
|
$single = $this->performanceResults['single_resolution'];
|
|
$this->output(sprintf(" Single Resolution: %.2fms avg (Target: <%.0fms)",
|
|
$single['avg_time_ms'], self::THRESHOLDS['single_resolution_ms']));
|
|
}
|
|
|
|
// Batch performance
|
|
if (isset($this->performanceResults['batch_100_resolution'])) {
|
|
$batch = $this->performanceResults['batch_100_resolution'];
|
|
$this->output(sprintf(" Batch 100 Resolutions: %.2fms total, %.2fms avg",
|
|
$batch['total_time_ms'], $batch['avg_per_resolution_ms']));
|
|
}
|
|
|
|
// Memory usage
|
|
if (isset($this->performanceResults['memory_usage'])) {
|
|
$memory = $this->performanceResults['memory_usage'];
|
|
$this->output(sprintf(" Memory Usage: %.2fMB peak (Target: <%.0fMB)",
|
|
$memory['peak_memory_used_mb'], self::THRESHOLDS['memory_usage_mb']));
|
|
}
|
|
|
|
// Database efficiency
|
|
if (isset($this->performanceResults['database_efficiency'])) {
|
|
$db = $this->performanceResults['database_efficiency'];
|
|
$this->output(sprintf(" Database Queries: %d per resolution (Target: <%d)",
|
|
$db['total_queries'], self::THRESHOLDS['db_queries_per_resolution']));
|
|
}
|
|
|
|
// Throughput
|
|
if (isset($this->performanceResults['large_dataset'])) {
|
|
$throughput = $this->performanceResults['large_dataset'];
|
|
$this->output(sprintf(" Throughput: %.2f resolutions/second (Target: >=10/sec)",
|
|
$throughput['throughput_per_second']));
|
|
}
|
|
|
|
$this->output("\n" . str_repeat("=", 70));
|
|
|
|
if ($successRate >= 90) {
|
|
$this->output("🎉 PERFORMANCE TESTS PASSED - System meets performance requirements");
|
|
exit(0);
|
|
} elseif ($successRate >= 70) {
|
|
$this->output("⚠️ PERFORMANCE WARNING - Some performance issues detected");
|
|
exit(1);
|
|
} else {
|
|
$this->output("❌ PERFORMANCE TESTS FAILED - Critical performance issues found");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the performance tests
|
|
$tester = new BomPerformanceTester();
|
|
$tester->run();
|