490 lines
16 KiB
PHP
490 lines
16 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Performance;
|
||
|
|
|
||
|
|
use App\Models\BomConditionRule;
|
||
|
|
use App\Models\Model;
|
||
|
|
use App\Models\ModelFormula;
|
||
|
|
use App\Models\ModelParameter;
|
||
|
|
use App\Models\User;
|
||
|
|
use App\Services\ProductFromModelService;
|
||
|
|
use Database\Seeders\ParameterBasedBomTestSeeder;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Laravel\Sanctum\Sanctum;
|
||
|
|
use Tests\TestCase;
|
||
|
|
|
||
|
|
class BomResolutionPerformanceTest extends TestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
private User $user;
|
||
|
|
private Model $performanceModel;
|
||
|
|
private ProductFromModelService $service;
|
||
|
|
|
||
|
|
protected function setUp(): void
|
||
|
|
{
|
||
|
|
parent::setUp();
|
||
|
|
|
||
|
|
$this->user = User::factory()->create(['tenant_id' => 1]);
|
||
|
|
Sanctum::actingAs($this->user);
|
||
|
|
|
||
|
|
$this->service = new ProductFromModelService();
|
||
|
|
$this->service->setTenantId(1)->setApiUserId(1);
|
||
|
|
|
||
|
|
// Set headers
|
||
|
|
$this->withHeaders([
|
||
|
|
'X-API-KEY' => config('app.api_key', 'test-api-key'),
|
||
|
|
'Accept' => 'application/json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Seed test data
|
||
|
|
$this->seed(ParameterBasedBomTestSeeder::class);
|
||
|
|
|
||
|
|
// Create performance test model with large dataset
|
||
|
|
$this->createPerformanceTestModel();
|
||
|
|
}
|
||
|
|
|
||
|
|
private function createPerformanceTestModel(): void
|
||
|
|
{
|
||
|
|
$this->performanceModel = Model::factory()->create([
|
||
|
|
'code' => 'PERF-TEST',
|
||
|
|
'name' => 'Performance Test Model',
|
||
|
|
'product_family' => 'SCREEN',
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Create many parameters (50)
|
||
|
|
for ($i = 1; $i <= 50; $i++) {
|
||
|
|
ModelParameter::factory()->create([
|
||
|
|
'model_id' => $this->performanceModel->id,
|
||
|
|
'name' => "param_{$i}",
|
||
|
|
'label' => "Parameter {$i}",
|
||
|
|
'type' => 'NUMBER',
|
||
|
|
'default_value' => '100',
|
||
|
|
'sort_order' => $i,
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create many formulas (30)
|
||
|
|
for ($i = 1; $i <= 30; $i++) {
|
||
|
|
ModelFormula::factory()->create([
|
||
|
|
'model_id' => $this->performanceModel->id,
|
||
|
|
'name' => "formula_{$i}",
|
||
|
|
'expression' => "param_1 + param_2 + {$i}",
|
||
|
|
'description' => "Formula {$i}",
|
||
|
|
'return_type' => 'NUMBER',
|
||
|
|
'sort_order' => $i,
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create many condition rules (100)
|
||
|
|
for ($i = 1; $i <= 100; $i++) {
|
||
|
|
BomConditionRule::factory()->create([
|
||
|
|
'model_id' => $this->performanceModel->id,
|
||
|
|
'name' => "rule_{$i}",
|
||
|
|
'condition_expression' => "formula_1 > {$i}",
|
||
|
|
'component_code' => "COMPONENT_{$i}",
|
||
|
|
'quantity_expression' => '1',
|
||
|
|
'priority' => $i,
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_resolves_simple_bom_within_performance_threshold()
|
||
|
|
{
|
||
|
|
// Use KSS01 model for simple test
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => 1000,
|
||
|
|
'H0' => 800,
|
||
|
|
'screen_type' => 'SCREEN',
|
||
|
|
'install_type' => 'WALL',
|
||
|
|
'power_source' => 'AC',
|
||
|
|
];
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$executionTime = microtime(true) - $startTime;
|
||
|
|
|
||
|
|
// Should complete within 500ms for simple model
|
||
|
|
$this->assertLessThan(0.5, $executionTime, 'Simple BOM resolution took too long');
|
||
|
|
$response->assertOk();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_complex_bom_resolution_efficiently()
|
||
|
|
{
|
||
|
|
// Create parameters for all 50 parameters
|
||
|
|
$inputParams = [];
|
||
|
|
for ($i = 1; $i <= 50; $i++) {
|
||
|
|
$inputParams["param_{$i}"] = 100 + $i;
|
||
|
|
}
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$executionTime = microtime(true) - $startTime;
|
||
|
|
|
||
|
|
// Should complete within 2 seconds even for complex model
|
||
|
|
$this->assertLessThan(2.0, $executionTime, 'Complex BOM resolution took too long');
|
||
|
|
$response->assertOk();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_concurrent_bom_resolutions()
|
||
|
|
{
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => 1000,
|
||
|
|
'H0' => 800,
|
||
|
|
'screen_type' => 'SCREEN',
|
||
|
|
'install_type' => 'WALL',
|
||
|
|
'power_source' => 'AC',
|
||
|
|
];
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
$responses = [];
|
||
|
|
|
||
|
|
// Simulate 10 concurrent requests
|
||
|
|
for ($i = 0; $i < 10; $i++) {
|
||
|
|
$responses[] = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$totalTime = microtime(true) - $startTime;
|
||
|
|
|
||
|
|
// All requests should complete successfully
|
||
|
|
foreach ($responses as $response) {
|
||
|
|
$response->assertOk();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Total time for 10 concurrent requests should be reasonable
|
||
|
|
$this->assertLessThan(5.0, $totalTime, 'Concurrent BOM resolutions took too long');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_optimizes_formula_evaluation_with_caching()
|
||
|
|
{
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => 1000,
|
||
|
|
'H0' => 800,
|
||
|
|
'screen_type' => 'SCREEN',
|
||
|
|
'install_type' => 'WALL',
|
||
|
|
'power_source' => 'AC',
|
||
|
|
];
|
||
|
|
|
||
|
|
// First request (cold cache)
|
||
|
|
$startTime1 = microtime(true);
|
||
|
|
$response1 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
$time1 = microtime(true) - $startTime1;
|
||
|
|
|
||
|
|
// Second request with same parameters (warm cache)
|
||
|
|
$startTime2 = microtime(true);
|
||
|
|
$response2 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
$time2 = microtime(true) - $startTime2;
|
||
|
|
|
||
|
|
// Both should succeed
|
||
|
|
$response1->assertOk();
|
||
|
|
$response2->assertOk();
|
||
|
|
|
||
|
|
// Results should be identical
|
||
|
|
$this->assertEquals($response1->json('data'), $response2->json('data'));
|
||
|
|
|
||
|
|
// Second request should be faster (with caching)
|
||
|
|
$this->assertLessThan($time1, $time2 * 1.5, 'Caching is not improving performance');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_measures_memory_usage_during_bom_resolution()
|
||
|
|
{
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => 1000,
|
||
|
|
'H0' => 800,
|
||
|
|
'screen_type' => 'SCREEN',
|
||
|
|
'install_type' => 'WALL',
|
||
|
|
'power_source' => 'AC',
|
||
|
|
];
|
||
|
|
|
||
|
|
$memoryBefore = memory_get_usage(true);
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$memoryAfter = memory_get_usage(true);
|
||
|
|
$memoryUsed = $memoryAfter - $memoryBefore;
|
||
|
|
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Memory usage should be reasonable (less than 50MB for simple BOM)
|
||
|
|
$this->assertLessThan(50 * 1024 * 1024, $memoryUsed, 'Excessive memory usage detected');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_large_datasets_efficiently()
|
||
|
|
{
|
||
|
|
// Test with the performance model (50 params, 30 formulas, 100 rules)
|
||
|
|
$inputParams = [];
|
||
|
|
for ($i = 1; $i <= 50; $i++) {
|
||
|
|
$inputParams["param_{$i}"] = rand(50, 200);
|
||
|
|
}
|
||
|
|
|
||
|
|
$memoryBefore = memory_get_usage(true);
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$executionTime = microtime(true) - $startTime;
|
||
|
|
$memoryAfter = memory_get_usage(true);
|
||
|
|
$memoryUsed = $memoryAfter - $memoryBefore;
|
||
|
|
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Performance thresholds for large datasets
|
||
|
|
$this->assertLessThan(5.0, $executionTime, 'Large dataset processing took too long');
|
||
|
|
$this->assertLessThan(100 * 1024 * 1024, $memoryUsed, 'Excessive memory usage for large dataset');
|
||
|
|
|
||
|
|
// Should return reasonable amount of data
|
||
|
|
$data = $response->json('data');
|
||
|
|
$this->assertArrayHasKey('calculated_formulas', $data);
|
||
|
|
$this->assertArrayHasKey('bom_items', $data);
|
||
|
|
$this->assertGreaterThan(0, count($data['bom_items']));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_benchmarks_formula_evaluation_complexity()
|
||
|
|
{
|
||
|
|
// Test various formula complexities
|
||
|
|
$complexityTests = [
|
||
|
|
'simple' => 'param_1 + param_2',
|
||
|
|
'medium' => 'param_1 * param_2 + param_3 / param_4',
|
||
|
|
'complex' => 'CEIL(param_1 / 600) + FLOOR(param_2 * 1.5) + IF(param_3 > 100, param_4, param_5)',
|
||
|
|
];
|
||
|
|
|
||
|
|
$benchmarks = [];
|
||
|
|
|
||
|
|
foreach ($complexityTests as $complexity => $expression) {
|
||
|
|
// Create test formula
|
||
|
|
$formula = ModelFormula::factory()->create([
|
||
|
|
'model_id' => $this->performanceModel->id,
|
||
|
|
'name' => "benchmark_{$complexity}",
|
||
|
|
'expression' => $expression,
|
||
|
|
'sort_order' => 999,
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'param_1' => 1000,
|
||
|
|
'param_2' => 800,
|
||
|
|
'param_3' => 150,
|
||
|
|
'param_4' => 200,
|
||
|
|
'param_5' => 50,
|
||
|
|
];
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
// Evaluate formula multiple times to get average
|
||
|
|
for ($i = 0; $i < 100; $i++) {
|
||
|
|
try {
|
||
|
|
$this->service->evaluateFormula($formula, $inputParams);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Some complex formulas might fail, that's okay for benchmarking
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$avgTime = (microtime(true) - $startTime) / 100;
|
||
|
|
$benchmarks[$complexity] = $avgTime;
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
$formula->delete();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Complex formulas should still execute reasonably fast
|
||
|
|
$this->assertLessThan(0.01, $benchmarks['simple'], 'Simple formula evaluation too slow');
|
||
|
|
$this->assertLessThan(0.02, $benchmarks['medium'], 'Medium formula evaluation too slow');
|
||
|
|
$this->assertLessThan(0.05, $benchmarks['complex'], 'Complex formula evaluation too slow');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_scales_with_increasing_rule_count()
|
||
|
|
{
|
||
|
|
// Test BOM resolution with different rule counts
|
||
|
|
$scalingTests = [10, 50, 100];
|
||
|
|
$scalingResults = [];
|
||
|
|
|
||
|
|
foreach ($scalingTests as $ruleCount) {
|
||
|
|
// Create test model with specific rule count
|
||
|
|
$testModel = Model::factory()->create([
|
||
|
|
'code' => "SCALE_TEST_{$ruleCount}",
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Create basic parameters
|
||
|
|
ModelParameter::factory()->create([
|
||
|
|
'model_id' => $testModel->id,
|
||
|
|
'name' => 'test_param',
|
||
|
|
'type' => 'NUMBER',
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Create test formula
|
||
|
|
ModelFormula::factory()->create([
|
||
|
|
'model_id' => $testModel->id,
|
||
|
|
'name' => 'test_formula',
|
||
|
|
'expression' => 'test_param * 2',
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Create specified number of rules
|
||
|
|
for ($i = 1; $i <= $ruleCount; $i++) {
|
||
|
|
BomConditionRule::factory()->create([
|
||
|
|
'model_id' => $testModel->id,
|
||
|
|
'condition_expression' => "test_formula > {$i}",
|
||
|
|
'component_code' => "COMP_{$i}",
|
||
|
|
'priority' => $i,
|
||
|
|
'tenant_id' => 1,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$testModel->id}/bom/resolve", [
|
||
|
|
'parameters' => ['test_param' => 100]
|
||
|
|
]);
|
||
|
|
|
||
|
|
$executionTime = microtime(true) - $startTime;
|
||
|
|
$scalingResults[$ruleCount] = $executionTime;
|
||
|
|
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
$testModel->delete();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Execution time should scale reasonably (not exponentially)
|
||
|
|
$ratio50to10 = $scalingResults[50] / $scalingResults[10];
|
||
|
|
$ratio100to50 = $scalingResults[100] / $scalingResults[50];
|
||
|
|
|
||
|
|
// Should not scale worse than linearly
|
||
|
|
$this->assertLessThan(10, $ratio50to10, 'Poor scaling from 10 to 50 rules');
|
||
|
|
$this->assertLessThan(5, $ratio100to50, 'Poor scaling from 50 to 100 rules');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_stress_test_scenarios()
|
||
|
|
{
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
// Stress test with many rapid requests
|
||
|
|
$stressTestCount = 50;
|
||
|
|
$successCount = 0;
|
||
|
|
$errors = [];
|
||
|
|
$totalTime = 0;
|
||
|
|
|
||
|
|
for ($i = 0; $i < $stressTestCount; $i++) {
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => rand(500, 3000),
|
||
|
|
'H0' => rand(400, 2000),
|
||
|
|
'screen_type' => rand(0, 1) ? 'SCREEN' : 'SLAT',
|
||
|
|
'install_type' => ['WALL', 'SIDE', 'MIXED'][rand(0, 2)],
|
||
|
|
'power_source' => ['AC', 'DC', 'MANUAL'][rand(0, 2)],
|
||
|
|
];
|
||
|
|
|
||
|
|
$startTime = microtime(true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$executionTime = microtime(true) - $startTime;
|
||
|
|
$totalTime += $executionTime;
|
||
|
|
|
||
|
|
if ($response->isSuccessful()) {
|
||
|
|
$successCount++;
|
||
|
|
} else {
|
||
|
|
$errors[] = $response->status();
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$errors[] = $e->getMessage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$avgTime = $totalTime / $stressTestCount;
|
||
|
|
$successRate = ($successCount / $stressTestCount) * 100;
|
||
|
|
|
||
|
|
// Stress test requirements
|
||
|
|
$this->assertGreaterThanOrEqual(95, $successRate, 'Success rate too low under stress');
|
||
|
|
$this->assertLessThan(1.0, $avgTime, 'Average response time too high under stress');
|
||
|
|
|
||
|
|
if (count($errors) > 0) {
|
||
|
|
$this->addToAssertionCount(1); // Just to show we're tracking errors
|
||
|
|
// Log errors for analysis
|
||
|
|
error_log('Stress test errors: ' . json_encode(array_unique($errors)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_monitors_database_query_performance()
|
||
|
|
{
|
||
|
|
$kss01 = Model::where('code', 'KSS01')->first();
|
||
|
|
|
||
|
|
$inputParams = [
|
||
|
|
'W0' => 1000,
|
||
|
|
'H0' => 800,
|
||
|
|
'screen_type' => 'SCREEN',
|
||
|
|
'install_type' => 'WALL',
|
||
|
|
'power_source' => 'AC',
|
||
|
|
];
|
||
|
|
|
||
|
|
// Enable query logging
|
||
|
|
\DB::enableQueryLog();
|
||
|
|
|
||
|
|
$response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [
|
||
|
|
'parameters' => $inputParams
|
||
|
|
]);
|
||
|
|
|
||
|
|
$queries = \DB::getQueryLog();
|
||
|
|
\DB::disableQueryLog();
|
||
|
|
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Analyze query performance
|
||
|
|
$queryCount = count($queries);
|
||
|
|
$totalQueryTime = array_sum(array_column($queries, 'time'));
|
||
|
|
|
||
|
|
// Should not have excessive queries (N+1 problem)
|
||
|
|
$this->assertLessThan(50, $queryCount, 'Too many database queries');
|
||
|
|
|
||
|
|
// Total query time should be reasonable
|
||
|
|
$this->assertLessThan(500, $totalQueryTime, 'Database queries taking too long');
|
||
|
|
|
||
|
|
// Check for slow queries
|
||
|
|
$slowQueries = array_filter($queries, fn($query) => $query['time'] > 100);
|
||
|
|
$this->assertEmpty($slowQueries, 'Slow queries detected: ' . json_encode($slowQueries));
|
||
|
|
}
|
||
|
|
}
|