Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/phpbench_to_json.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* Convert PHPBench XML output to the JSON format expected by
* benchmark-action/github-action-benchmark (customSmallerIsBetter).
*
* Usage:
* php phpbench_to_json.php phpbench.xml > output.json
*
* The script reads the XML dump produced by PHPBench's --dump-file option
* and emits a JSON array where each entry has:
* - name: benchmark class short name + subject (e.g. "DiagnosticsBench::benchDiagnostics")
* - unit: "μs" (microseconds)
* - value: mean time in microseconds
* - range: "± {relative standard deviation}%"
* - extra: iteration count and revision count
*/

if ($argc < 2) {
fwrite(STDERR, "Usage: php phpbench_to_json.php <phpbench-dump.xml>\n");
exit(1);
}

$file = $argv[1];

if (!file_exists($file)) {
fwrite(STDERR, "Error: file not found: {$file}\n");
exit(1);
}

$xml = simplexml_load_file($file);

if ($xml === false) {
fwrite(STDERR, "Error: could not parse XML file: {$file}\n");
exit(1);
}

$results = [];

foreach ($xml->suite as $suite) {
foreach ($suite->benchmark as $benchmark) {
$className = (string) $benchmark['class'];

// Use the short class name for readability.
$shortName = $className;
if (($pos = strrpos($className, '\\')) !== false) {
$shortName = substr($className, $pos + 1);
}

foreach ($benchmark->subject as $subject) {
$subjectName = (string) $subject['name'];

foreach ($subject->variant as $variant) {
// Build a descriptive name including parameter set if present.
$paramDesc = '';
if (isset($variant->parameter_set)) {
$params = [];
foreach ($variant->parameter_set->parameter as $param) {
$params[] = (string) $param['value'];
}
if (!empty($params)) {
$paramDesc = ' (' . implode(', ', $params) . ')';
}
}

$name = $shortName . '::' . $subjectName . $paramDesc;

$stats = $variant->stats;

if (!$stats) {
// Fall back to computing from iterations if stats element is missing.
$times = [];
foreach ($variant->iteration as $iteration) {
$revs = (int) $iteration['time-revs'];
$netTime = (float) $iteration['time-net'];
// time-net is total time for all revs in microseconds.
$times[] = $revs > 0 ? $netTime / $revs : $netTime;
}

if (empty($times)) {
continue;
}

$mean = array_sum($times) / count($times);
$rstdev = 0;
if (count($times) > 1 && $mean > 0) {
$variance = 0;
foreach ($times as $t) {
$variance += ($t - $mean) ** 2;
}
$variance /= count($times);
$rstdev = (sqrt($variance) / $mean) * 100;
}

$results[] = [
'name' => $name,
'unit' => 'μs',
'value' => round($mean, 3),
'range' => '± ' . round($rstdev, 2) . '%',
'extra' => count($times) . ' iterations',
];
} else {
$mean = (float) $stats['mean'];
$rstdev = (float) $stats['rstdev'];
$iterations = (int) $variant['iterations'];
$revs = (int) $variant['revs'];

$results[] = [
'name' => $name,
'unit' => 'μs',
'value' => round($mean, 3),
'range' => '± ' . round($rstdev, 2) . '%',
'extra' => $iterations . ' iterations, ' . $revs . ' revs',
];
}
}
}
}
}

echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
112 changes: 112 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Benchmark

on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
benchmark:
name: Benchmark
runs-on: ubuntu-latest
permissions:
contents: write
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.1"
tools: composer:v2

- name: Composer install
uses: ramsey/composer-install@v2
with:
composer-options: "--no-scripts"

- name: Ensure gh-pages branch exists
run: |
if ! git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
empty_tree="$(git hash-object -t tree /dev/null)"
commit="$(git commit-tree "$empty_tree" -m 'Initial gh-pages branch')"
git push origin "$commit:refs/heads/gh-pages"
fi

- name: Run PHPBench
run: vendor/bin/phpbench run --progress=plain --dump-file=phpbench.xml

- name: Convert results to JSON
run: php .github/phpbench_to_json.php phpbench.xml > output.json

- name: Store benchmark result
uses: benchmark-action/github-action-benchmark@v1
with:
name: Phpactor Benchmarks
tool: customSmallerIsBetter
output-file-path: output.json
gh-pages-branch: gh-pages
benchmark-data-dir-path: dev/bench
auto-push: true
github-token: ${{ secrets.GITHUB_TOKEN }}

benchmark-pr:
name: Benchmark (PR comparison)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.1"
tools: composer:v2

- name: Composer install
uses: ramsey/composer-install@v2
with:
composer-options: "--no-scripts"

- name: Check gh-pages branch exists
id: check-gh-pages
run: |
if git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "⚠️ gh-pages branch does not exist yet — skipping PR benchmark comparison"
fi

- name: Run PHPBench
if: steps.check-gh-pages.outputs.exists == 'true'
run: vendor/bin/phpbench run --progress=plain --dump-file=phpbench.xml

- name: Convert results to JSON
if: steps.check-gh-pages.outputs.exists == 'true'
run: php .github/phpbench_to_json.php phpbench.xml > output.json

- name: Compare against baseline
if: steps.check-gh-pages.outputs.exists == 'true'
uses: benchmark-action/github-action-benchmark@v1
with:
name: Phpactor Benchmarks
tool: customSmallerIsBetter
output-file-path: output.json
gh-pages-branch: gh-pages
benchmark-data-dir-path: dev/bench
auto-push: false
github-token: ${{ secrets.GITHUB_TOKEN }}
comment-on-alert: true
alert-threshold: "130%"
fail-on-alert: true