From 8112e3ded69d91f44ce26b0df1628fbeb0bee748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Fri, 17 Oct 2025 09:41:13 +0200 Subject: [PATCH 1/5] feat: Add PHPBench benchmarking workflow and tests for ArrayDiffMultidimensional --- .github/workflows/benchmark.yml | 106 ++++ phpbench.json | 4 + tests/Benchmark/ArrayCompareBenchmark.php | 583 ++++++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 phpbench.json create mode 100644 tests/Benchmark/ArrayCompareBenchmark.php diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..5026d58 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,106 @@ +name: benchmark + +on: + pull_request: + branches: + - master + types: [opened, synchronize, reopened] + +jobs: + benchmark: + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout the PR branch + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for comparison + + # Step 2: Setup PHP + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: zip, curl, mbstring, xml + coverage: none + + # Step 3: Install dependencies for PR branch + - name: Install Composer dependencies + run: | + composer install --prefer-dist --no-progress --no-suggest + composer require --dev phpbench/phpbench + + # Step 4: Run benchmarks on PR branch + - name: Run benchmarks on PR branch + run: | + ./vendor/bin/phpbench run \ + --report=default \ + --tag=pr \ + --retry-threshold=5 \ + --iterations=10 \ + --output=json > pr-results.json + + # Step 5: Checkout base branch + - name: Checkout base branch + run: | + git fetch origin ${{ github.base_ref }} + git checkout origin/${{ github.base_ref }} + + # Step 6: Install dependencies for base branch + - name: Install Composer dependencies (base) + run: | + composer install --prefer-dist --no-progress --no-suggest + composer require --dev phpbench/phpbench + + # Step 7: Run benchmarks on base branch + - name: Run benchmarks on base branch + run: | + ./vendor/bin/phpbench run \ + --report=default \ + --tag=base \ + --retry-threshold=5 \ + --iterations=10 + + # Step 8: Checkout PR branch again + - name: Checkout PR branch again + run: git checkout ${{ github.head_ref }} + + # Step 9: Compare benchmarks + - name: Compare benchmarks with baseline + id: compare + run: | + ./vendor/bin/phpbench run \ + --report=aggregate \ + --ref=base \ + --retry-threshold=5 \ + --iterations=10 \ + --tag=pr \ + --assert="mode(variant.time.avg) <= mode(baseline.time.avg) +/- 10%" \ + | tee benchmark-comparison.txt + continue-on-error: true + + # Step 10: Post results as PR comment + - name: Comment PR with benchmark results + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const results = fs.readFileSync('benchmark-comparison.txt', 'utf8'); + + const body = `## 📊 Benchmark Results\n\n\`\`\`\n${results}\n\`\`\`\n\n**Note:** Benchmarks compare PR against \`${{ github.base_ref }}\` branch.\nPerformance regression threshold: ±10%`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + + # Step 11: Fail if performance degrades + - name: Check benchmark assertions + if: steps.compare.outcome == 'failure' + run: | + echo "::error::Performance regression detected! Benchmarks exceeded acceptable threshold." + exit 1 diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..d2d1411 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php" +} diff --git a/tests/Benchmark/ArrayCompareBenchmark.php b/tests/Benchmark/ArrayCompareBenchmark.php new file mode 100644 index 0000000..5b06701 --- /dev/null +++ b/tests/Benchmark/ArrayCompareBenchmark.php @@ -0,0 +1,583 @@ +smallArray1 = [ + 'name' => 'John', + 'age' => 30, + 'active' => true, + ]; + + $this->smallArray2 = [ + 'name' => 'John', + 'age' => 31, + 'active' => false, + ]; + + // Medium arrays (realistic user profile) + $this->mediumArray1 = [ + 'id' => 123, + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'age' => 30, + 'address' => [ + 'street' => '123 Main St', + 'city' => 'New York', + 'state' => 'NY', + 'zip' => '10001', + 'country' => 'USA', + ], + 'hobbies' => ['reading', 'traveling', 'swimming'], + 'metadata' => [ + 'created_at' => '2023-01-01', + 'updated_at' => '2023-06-15', + 'version' => 2, + 'flags' => ['premium' => true, 'verified' => true], + ], + ]; + + $this->mediumArray2 = [ + 'id' => 123, + 'name' => 'John Doe', + 'email' => 'john.new@example.com', + 'age' => 31, + 'address' => [ + 'street' => '456 Oak Ave', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '90001', + 'country' => 'USA', + ], + 'hobbies' => ['reading', 'traveling', 'cycling'], + 'metadata' => [ + 'created_at' => '2023-01-01', + 'updated_at' => '2023-12-20', + 'version' => 3, + 'flags' => ['premium' => true, 'verified' => false], + ], + ]; + + // Large arrays (complex configuration) + $this->largeArray1 = [ + 'application' => [ + 'name' => 'MyApp', + 'version' => '2.5.1', + 'environment' => 'production', + 'debug' => false, + ], + 'database' => [ + 'default' => [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'port' => 3306, + 'database' => 'myapp', + 'username' => 'root', + 'password' => 'secret', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'options' => [ + 'timeout' => 30, + 'retry' => 3, + 'ssl' => false, + ], + ], + 'redis' => [ + 'driver' => 'redis', + 'host' => '127.0.0.1', + 'port' => 6379, + 'database' => 0, + ], + ], + 'cache' => [ + 'driver' => 'redis', + 'ttl' => 3600, + 'prefix' => 'myapp_cache', + ], + 'mail' => [ + 'driver' => 'smtp', + 'host' => 'smtp.mailtrap.io', + 'port' => 2525, + 'encryption' => 'tls', + 'from' => [ + 'address' => 'noreply@example.com', + 'name' => 'MyApp', + ], + ], + 'services' => [ + 'analytics' => ['enabled' => true, 'key' => 'GA-12345'], + 'monitoring' => ['enabled' => true, 'endpoint' => 'https://monitor.example.com'], + 'cdn' => ['enabled' => false, 'url' => ''], + ], + ]; + + $this->largeArray2 = [ + 'application' => [ + 'name' => 'MyApp', + 'version' => '2.6.0', + 'environment' => 'production', + 'debug' => false, + ], + 'database' => [ + 'default' => [ + 'driver' => 'mysql', + 'host' => 'db.example.com', + 'port' => 3306, + 'database' => 'myapp_prod', + 'username' => 'appuser', + 'password' => 'new_secret', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'options' => [ + 'timeout' => 60, + 'retry' => 5, + 'ssl' => true, + ], + ], + 'redis' => [ + 'driver' => 'redis', + 'host' => 'redis.example.com', + 'port' => 6379, + 'database' => 1, + ], + ], + 'cache' => [ + 'driver' => 'memcached', + 'ttl' => 7200, + 'prefix' => 'myapp_cache_v2', + ], + 'mail' => [ + 'driver' => 'ses', + 'region' => 'us-east-1', + 'from' => [ + 'address' => 'noreply@example.com', + 'name' => 'MyApp Notifications', + ], + ], + 'services' => [ + 'analytics' => ['enabled' => true, 'key' => 'GA-67890'], + 'monitoring' => ['enabled' => true, 'endpoint' => 'https://monitor.newrelic.com'], + 'cdn' => ['enabled' => true, 'url' => 'https://cdn.example.com'], + ], + ]; + + // Deeply nested arrays (10 levels deep) + $this->deeplyNestedArray1 = [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'level4' => [ + 'level5' => [ + 'level6' => [ + 'level7' => [ + 'level8' => [ + 'level9' => [ + 'level10' => ['value' => 'deep_value_1', 'id' => 1], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ]; + + $this->deeplyNestedArray2 = [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'level4' => [ + 'level5' => [ + 'level6' => [ + 'level7' => [ + 'level8' => [ + 'level9' => [ + 'level10' => ['value' => 'deep_value_2', 'id' => 2], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ]; + + // Wide arrays (many keys at same level) + $this->wideArray1 = []; + $this->wideArray2 = []; + + for ($i = 0; $i < 100; $i++) { + $this->wideArray1["key_$i"] = [ + 'id' => $i, + 'value' => "value_$i", + 'active' => true, + 'metadata' => ['created' => '2023-01-01', 'updated' => '2023-06-15'], + ]; + + $this->wideArray2["key_$i"] = [ + 'id' => $i, + 'value' => "value_" . ($i + 1), + 'active' => $i % 2 === 0, + 'metadata' => ['created' => '2023-01-01', 'updated' => '2023-12-20'], + ]; + } + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchSmallArrayStrictComparison(): void + { + ArrayDiffMultidimensional::strictComparison($this->smallArray1, $this->smallArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchSmallArrayLooseComparison(): void + { + ArrayDiffMultidimensional::looseComparison($this->smallArray1, $this->smallArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchMediumArrayStrictComparison(): void + { + ArrayDiffMultidimensional::strictComparison($this->mediumArray1, $this->mediumArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchMediumArrayLooseComparison(): void + { + ArrayDiffMultidimensional::looseComparison($this->mediumArray1, $this->mediumArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(500) + * @Iterations(5) + */ + public function benchLargeArrayStrictComparison(): void + { + ArrayDiffMultidimensional::strictComparison($this->largeArray1, $this->largeArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(500) + * @Iterations(5) + */ + public function benchLargeArrayLooseComparison(): void + { + ArrayDiffMultidimensional::looseComparison($this->largeArray1, $this->largeArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(500) + * @Iterations(5) + */ + public function benchDeeplyNestedArrayComparison(): void + { + ArrayDiffMultidimensional::compare($this->deeplyNestedArray1, $this->deeplyNestedArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(200) + * @Iterations(5) + */ + public function benchWideArrayComparison(): void + { + ArrayDiffMultidimensional::compare($this->wideArray1, $this->wideArray2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchIdenticalArrays(): void + { + ArrayDiffMultidimensional::compare($this->smallArray1, $this->smallArray1); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchCompletelyDifferentArrays(): void + { + $a = ['name' => 'Alice', 'age' => 25, 'city' => 'Boston']; + $b = ['country' => 'USA', 'zip' => '02101', 'active' => true]; + + ArrayDiffMultidimensional::compare($a, $b); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(500) + * @Iterations(5) + */ + public function benchMixedDataTypes(): void + { + $a = [ + 'int' => 42, + 'float' => 3.14159, + 'string' => 'hello', + 'bool' => true, + 'null' => null, + 'array' => [1, 2, 3], + 'nested' => [ + 'deep' => [ + 'value' => 'test', + 'number' => 123.456, + ], + ], + ]; + + $b = [ + 'int' => 43, + 'float' => 2.71828, + 'string' => 'world', + 'bool' => false, + 'null' => null, + 'array' => [1, 2, 4], + 'nested' => [ + 'deep' => [ + 'value' => 'test', + 'number' => 789.012, + ], + ], + ]; + + ArrayDiffMultidimensional::compare($a, $b); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(200) + * @Iterations(5) + */ + public function benchRealisticUserDataset(): void + { + $users1 = []; + $users2 = []; + + for ($i = 0; $i < 50; $i++) { + $users1[$i] = [ + 'id' => $i, + 'name' => "User $i", + 'email' => "user$i@example.com", + 'age' => 20 + $i, + 'active' => true, + 'profile' => [ + 'bio' => "Biography for user $i", + 'preferences' => [ + 'theme' => 'light', + 'notifications' => true, + 'language' => 'en', + ], + ], + ]; + + $users2[$i] = [ + 'id' => $i, + 'name' => "User $i", + 'email' => "user$i@example.com", + 'age' => 21 + $i, + 'active' => $i % 2 === 0, + 'profile' => [ + 'bio' => "Updated biography for user $i", + 'preferences' => [ + 'theme' => $i % 3 === 0 ? 'dark' : 'light', + 'notifications' => true, + 'language' => 'en', + ], + ], + ]; + } + + ArrayDiffMultidimensional::compare($users1, $users2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(100) + * @Iterations(5) + */ + public function benchComplexNestedConfiguration(): void + { + $config1 = $this->generateComplexConfig(1); + $config2 = $this->generateComplexConfig(2); + + ArrayDiffMultidimensional::compare($config1, $config2); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(1000) + * @Iterations(5) + */ + public function benchEmptyArrayComparison(): void + { + $a = ['key1' => 'value1', 'key2' => 'value2']; + $b = []; + + ArrayDiffMultidimensional::compare($a, $b); + } + + /** + * @BeforeMethods({"setUp"}) + * @Revs(500) + * @Iterations(5) + */ + public function benchArrayWithNumericKeys(): void + { + $a = [ + ['id' => 1, 'name' => 'Item 1', 'value' => 100], + ['id' => 2, 'name' => 'Item 2', 'value' => 200], + ['id' => 3, 'name' => 'Item 3', 'value' => 300], + ]; + + $b = [ + ['id' => 1, 'name' => 'Item 1', 'value' => 150], + ['id' => 2, 'name' => 'Item 2 Updated', 'value' => 200], + ['id' => 3, 'name' => 'Item 3', 'value' => 350], + ]; + + ArrayDiffMultidimensional::compare($a, $b); + } + + /** + * Helper method to generate complex configuration + */ + private function generateComplexConfig($variant) + { + return [ + 'app' => [ + 'name' => 'TestApp', + 'version' => "1.0.$variant", + 'env' => 'production', + 'debug' => false, + 'url' => "https://example$variant.com", + ], + 'database' => [ + 'connections' => [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => "db$variant.example.com", + 'port' => 3306, + 'database' => "app_db_$variant", + 'username' => 'dbuser', + 'password' => "secret_$variant", + 'options' => [ + 'timeout' => 30 * $variant, + 'retry' => 3, + 'pool' => [ + 'min' => 5, + 'max' => 20 + $variant, + ], + ], + ], + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => "pg$variant.example.com", + 'port' => 5432, + 'database' => "app_pg_$variant", + ], + ], + ], + 'cache' => [ + 'default' => 'redis', + 'stores' => [ + 'redis' => [ + 'driver' => 'redis', + 'connection' => [ + 'host' => "redis$variant.example.com", + 'port' => 6379, + 'database' => $variant, + ], + 'options' => [ + 'ttl' => 3600 * $variant, + 'prefix' => "cache_v$variant", + ], + ], + 'memcached' => [ + 'driver' => 'memcached', + 'servers' => [ + ['host' => "mc1.example.com", 'port' => 11211, 'weight' => 100], + ['host' => "mc2.example.com", 'port' => 11211, 'weight' => 50 * $variant], + ], + ], + ], + ], + 'services' => [ + 'mailer' => [ + 'driver' => 'smtp', + 'host' => "smtp$variant.example.com", + 'port' => 587, + 'encryption' => 'tls', + 'from' => [ + 'address' => "noreply@example$variant.com", + 'name' => "TestApp v$variant", + ], + ], + 'queue' => [ + 'driver' => 'sqs', + 'region' => 'us-east-1', + 'queue' => "app-queue-$variant", + ], + ], + 'logging' => [ + 'channels' => [ + 'stack' => ['driver' => 'stack', 'channels' => ['single', 'slack']], + 'single' => ['driver' => 'single', 'path' => "/var/log/app-$variant.log", 'level' => 'debug'], + 'slack' => ['driver' => 'slack', 'url' => "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX$variant"], + ], + ], + ]; + } +} From 339064a821d49dd98cb71ee5ff721e27a66eb7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Fri, 17 Oct 2025 09:44:39 +0200 Subject: [PATCH 2/5] fix: Correct environment variable usage for branch checkouts in benchmark workflow --- .github/workflows/benchmark.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5026d58..698476b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -44,8 +44,10 @@ jobs: # Step 5: Checkout base branch - name: Checkout base branch run: | - git fetch origin ${{ github.base_ref }} - git checkout origin/${{ github.base_ref }} + git fetch origin "$BASE_REF" + git checkout "origin/$BASE_REF" + env: + BASE_REF: ${{ github.base_ref }} # Step 6: Install dependencies for base branch - name: Install Composer dependencies (base) @@ -64,7 +66,9 @@ jobs: # Step 8: Checkout PR branch again - name: Checkout PR branch again - run: git checkout ${{ github.head_ref }} + run: git checkout "$HEAD_REF" + env: + HEAD_REF: ${{ github.head_ref }} # Step 9: Compare benchmarks - name: Compare benchmarks with baseline From 751c4e66abf87be06095e92a625d5261b0cd4c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Fri, 17 Oct 2025 09:48:17 +0200 Subject: [PATCH 3/5] fix: Update PHP setup action and specify benchmark test path in workflow --- .github/workflows/benchmark.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 698476b..08076fc 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -19,7 +19,7 @@ jobs: # Step 2: Setup PHP - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f with: php-version: '8.4' extensions: zip, curl, mbstring, xml @@ -34,7 +34,7 @@ jobs: # Step 4: Run benchmarks on PR branch - name: Run benchmarks on PR branch run: | - ./vendor/bin/phpbench run \ + ./vendor/bin/phpbench run tests/Benchmark \ --report=default \ --tag=pr \ --retry-threshold=5 \ @@ -58,7 +58,7 @@ jobs: # Step 7: Run benchmarks on base branch - name: Run benchmarks on base branch run: | - ./vendor/bin/phpbench run \ + ./vendor/bin/phpbench run tests/Benchmark \ --report=default \ --tag=base \ --retry-threshold=5 \ @@ -74,7 +74,7 @@ jobs: - name: Compare benchmarks with baseline id: compare run: | - ./vendor/bin/phpbench run \ + ./vendor/bin/phpbench run tests/Benchmark \ --report=aggregate \ --ref=base \ --retry-threshold=5 \ From 805f6077616d4a1f312ed0042dfa85759dfe66bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Fri, 17 Oct 2025 10:05:34 +0200 Subject: [PATCH 4/5] fix: Remove redundant output option from benchmark run command --- .github/workflows/benchmark.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 08076fc..2b713a4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -38,8 +38,7 @@ jobs: --report=default \ --tag=pr \ --retry-threshold=5 \ - --iterations=10 \ - --output=json > pr-results.json + --iterations=10 # Step 5: Checkout base branch - name: Checkout base branch From dc7bb85ecbf003036fc40f4a643984785b1063a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Fri, 17 Oct 2025 11:53:32 +0200 Subject: [PATCH 5/5] fix: Ensure benchmark comparison directory is created before running benchmarks --- .github/workflows/benchmark.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2b713a4..e6d592a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -34,6 +34,7 @@ jobs: # Step 4: Run benchmarks on PR branch - name: Run benchmarks on PR branch run: | + mkdir -p tests/Benchmark ./vendor/bin/phpbench run tests/Benchmark \ --report=default \ --tag=pr \ @@ -57,6 +58,7 @@ jobs: # Step 7: Run benchmarks on base branch - name: Run benchmarks on base branch run: | + mkdir -p tests/Benchmark ./vendor/bin/phpbench run tests/Benchmark \ --report=default \ --tag=base \ @@ -73,6 +75,7 @@ jobs: - name: Compare benchmarks with baseline id: compare run: | + mkdir -p tests/Benchmark ./vendor/bin/phpbench run tests/Benchmark \ --report=aggregate \ --ref=base \