Skip to content

Commit bf51278

Browse files
authored
Merge pull request #2545 from zephir-lang/#2541-phpbench
#2541 - Add PHPBench benchmarks
2 parents cc5a2e1 + ea7e3f6 commit bf51278

72 files changed

Lines changed: 3143 additions & 275 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,195 @@ jobs:
237237
!${{ github.workspace }}/ext/x64/Release/
238238
${{ github.workspace }}/tests/output/
239239
retention-days: 7
240+
241+
benchmark:
242+
name: "Benchmark"
243+
# Triggered by every push EXCEPT pushes to the default branch (which is
244+
# the comparison baseline) and tag pushes (releases). We deliberately
245+
# don't add `pull_request` to `on:` above because that would double-run
246+
# the other jobs (analyze, build-and-test) on each PR.
247+
if: github.event_name == 'push' && github.ref != 'refs/heads/development' && !startsWith(github.ref, 'refs/tags/')
248+
runs-on: ubuntu-24.04
249+
permissions:
250+
contents: read
251+
pull-requests: write
252+
env:
253+
# Held at 8.3 because PHPBench 1.2.x and its Symfony Console deps
254+
# haven't been updated for PHP 8.4's "Implicitly marking parameter as
255+
# nullable" deprecation. Running on 8.4+ floods the PR comment with
256+
# ~120 lines of upstream deprecation notices. Bumping when phpbench
257+
# ships a release that ports those signatures to `?Type` syntax.
258+
PHP_VERSION: '8.3'
259+
BASE_BRANCH: development
260+
BENCH_PHP_FLAGS: '-d extension=ext/modules/stub.so'
261+
262+
steps:
263+
- name: Checkout head
264+
uses: actions/checkout@v6
265+
with:
266+
# Need full history so we can switch to the base branch in-place.
267+
fetch-depth: 0
268+
269+
- name: Discover associated PR
270+
id: pr
271+
env:
272+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
273+
run: |
274+
branch="${GITHUB_REF#refs/heads/}"
275+
echo "branch=$branch" >> "$GITHUB_OUTPUT"
276+
# Look up the first open PR with this branch as head. Fork PRs are
277+
# surfaced as `owner:branch`, so we narrow by repo as well.
278+
pr_json=$(gh pr list \
279+
--repo "$GITHUB_REPOSITORY" \
280+
--head "$branch" \
281+
--state open \
282+
--json number,baseRefName,headRefName \
283+
--jq '.[0] // empty' 2>/dev/null || true)
284+
if [ -n "$pr_json" ]; then
285+
echo "has_pr=true" >> "$GITHUB_OUTPUT"
286+
echo "number=$(echo "$pr_json" | jq -r .number)" >> "$GITHUB_OUTPUT"
287+
echo "base=$(echo "$pr_json" | jq -r .baseRefName)" >> "$GITHUB_OUTPUT"
288+
else
289+
echo "has_pr=false" >> "$GITHUB_OUTPUT"
290+
echo "base=${BASE_BRANCH}" >> "$GITHUB_OUTPUT"
291+
echo "No open PR found for branch '$branch'; report will be uploaded as an artifact only."
292+
fi
293+
294+
- name: Setup PHP ${{ env.PHP_VERSION }}
295+
uses: shivammathur/setup-php@v2
296+
with:
297+
php-version: '${{ env.PHP_VERSION }}'
298+
extensions: mbstring, fileinfo, gmp, sqlite, pdo_sqlite, psr-${{ env.PSR_VERSION }}, zip, mysqli, zephir_parser-${{ env.ZEPHIR_PARSER_VERSION }}
299+
tools: pecl, phpize, php-config
300+
coverage: none
301+
ini-values: >-
302+
variables_order=EGPCS,
303+
enable_dl=On,
304+
allow_url_fopen=On,
305+
error_reporting=-1,
306+
memory_limit=1G,
307+
date.timezone=UTC
308+
309+
- name: Resolve base ref SHA
310+
id: base
311+
run: |
312+
base_sha=$(git rev-parse "origin/${{ steps.pr.outputs.base }}")
313+
echo "sha=$base_sha" >> "$GITHUB_OUTPUT"
314+
echo "Base: ${{ steps.pr.outputs.base }} ($base_sha)"
315+
echo "Head: $GITHUB_SHA"
316+
317+
- name: Build & bench base branch (${{ steps.pr.outputs.base }})
318+
run: |
319+
git checkout "${{ steps.base.outputs.sha }}"
320+
if [ ! -f tests/Benchmark/bootstrap.php ] || [ ! -f phpbench.json ]; then
321+
echo "BASE_HAS_BENCH=false" >> "$GITHUB_ENV"
322+
echo "Base branch does not yet include the benchmark suite; skipping baseline run."
323+
exit 0
324+
fi
325+
echo "BASE_HAS_BENCH=true" >> "$GITHUB_ENV"
326+
composer install --prefer-dist --no-interaction --no-ansi --no-progress
327+
php zephir fullclean >/dev/null 2>&1 || true
328+
php zephir build 2>&1 | tail -3
329+
php ${{ env.BENCH_PHP_FLAGS }} vendor/bin/phpbench run --tag=base --progress=none --no-interaction 2>&1 | tail -5
330+
# Preserve the phpbench storage across the upcoming branch switch.
331+
if [ -d .phpbench ]; then
332+
mkdir -p "$RUNNER_TEMP/phpbench"
333+
mv .phpbench "$RUNNER_TEMP/phpbench/storage"
334+
fi
335+
336+
- name: Restore head
337+
run: |
338+
git checkout "$GITHUB_SHA"
339+
if [ -d "$RUNNER_TEMP/phpbench/storage" ]; then
340+
mv "$RUNNER_TEMP/phpbench/storage" .phpbench
341+
fi
342+
343+
- name: Build & bench head
344+
id: bench
345+
run: |
346+
composer install --prefer-dist --no-interaction --no-ansi --no-progress
347+
php zephir fullclean >/dev/null 2>&1 || true
348+
php zephir build 2>&1 | tail -3
349+
350+
mkdir -p .phpbench
351+
if [ "$BASE_HAS_BENCH" = "true" ]; then
352+
php ${{ env.BENCH_PHP_FLAGS }} vendor/bin/phpbench run \
353+
--ref=base \
354+
--report=aggregate \
355+
--progress=none \
356+
--no-interaction \
357+
> .phpbench/report.txt 2>&1 || true
358+
else
359+
php ${{ env.BENCH_PHP_FLAGS }} vendor/bin/phpbench run \
360+
--report=aggregate \
361+
--progress=none \
362+
--no-interaction \
363+
> .phpbench/report.txt 2>&1 || true
364+
fi
365+
# Strip output that would clutter the PR comment:
366+
# - "Module already loaded" when the .so is in php.ini AND via -d
367+
# extension= on the command line;
368+
# - upstream PHP Deprecated/Notice/Warning lines (phpbench 1.2.x +
369+
# symfony/console haven't been ported to PHP 8.4's `?Type`
370+
# deprecation; out of scope to fix here).
371+
sed -i \
372+
-e '/Warning: Module .* is already loaded/d' \
373+
-e '/^PHP Deprecated:/d' \
374+
-e '/^PHP Notice:/d' \
375+
-e '/^Deprecated:/d' \
376+
.phpbench/report.txt
377+
378+
{
379+
echo "## Benchmark report"
380+
echo
381+
if [ "$BASE_HAS_BENCH" = "true" ]; then
382+
echo "Comparison against \`${{ steps.pr.outputs.base }}\` (\`${{ steps.base.outputs.sha }}\`) on \`${{ steps.pr.outputs.branch }}\` (\`$GITHUB_SHA\`)."
383+
echo "Each row's \`mode\` column shows the head-branch absolute throughput and the percent delta vs base. Positive deltas on \`Zephir*\` subjects mean head is faster; deltas on \`Php*\` baseline subjects are noise-floor signal."
384+
else
385+
echo "Base branch \`${{ steps.pr.outputs.base }}\` does not yet contain the benchmark suite; absolute throughput only."
386+
fi
387+
echo
388+
echo '```'
389+
cat .phpbench/report.txt
390+
echo '```'
391+
echo
392+
echo "_PHP ${{ env.PHP_VERSION }} on \`${{ runner.os }}\` (${{ runner.arch }}). Micro-benchmarks are noisy on shared runners (±5-20% per subject); treat any single-digit delta as inconclusive._"
393+
} > .phpbench/comment.md
394+
395+
- name: Upload benchmark artifacts
396+
if: always()
397+
uses: actions/upload-artifact@v7
398+
with:
399+
name: benchmark-report-${{ steps.pr.outputs.branch }}
400+
path: |
401+
.phpbench/report.txt
402+
.phpbench/comment.md
403+
retention-days: 14
404+
405+
- name: Post or update PR comment
406+
if: always() && steps.pr.outputs.has_pr == 'true'
407+
uses: actions/github-script@v7
408+
env:
409+
PR_NUMBER: ${{ steps.pr.outputs.number }}
410+
with:
411+
script: |
412+
const fs = require('fs');
413+
const body = fs.readFileSync('.phpbench/comment.md', 'utf8');
414+
const marker = '<!-- zephir-bench-report -->';
415+
const issue_number = parseInt(process.env.PR_NUMBER, 10);
416+
const { owner, repo } = context.repo;
417+
418+
const { data: comments } = await github.rest.issues.listComments({
419+
owner, repo, issue_number, per_page: 100,
420+
});
421+
const existing = comments.find(c => c.body && c.body.includes(marker));
422+
const finalBody = `${marker}\n${body}`;
423+
if (existing) {
424+
await github.rest.issues.updateComment({
425+
owner, repo, comment_id: existing.id, body: finalBody,
426+
});
427+
} else {
428+
await github.rest.issues.createComment({
429+
owner, repo, issue_number, body: finalBody,
430+
});
431+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ phpmd.xml
3434
phpunit.xml
3535
.phpunit.result.cache
3636
.php-cs-fixer.cache
37+
.phpbench/
3738

3839
# Build artefact
3940
zephir.phar

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
88

99
### Added
1010
- Compiler now recognizes the parser-emitted `yield` AST node (bare `yield;`, `yield expr;`, `yield key, value;`) [#1849](https://github.com/zephir-lang/zephir/issues/1849)
11+
- Added PHPBench-based runtime benchmarks suites under `tests/Benchmark/` [#2541](https://github.com/zephir-lang/zephir/issues/2541)
1112

1213
### Changed
1314
- `for k, v in expr` now skips the unreachable branch when the iterand's dynamic type is known [#1878](https://github.com/zephir-lang/zephir/issues/1878)

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"ext-pdo": "*",
3838
"ext-pdo_sqlite": "*",
3939
"ext-zip": "*",
40+
"phpbench/phpbench": "~1.2.0",
4041
"phpunit/phpunit": "^9.5",
4142
"psr/log": "1.1.*",
4243
"squizlabs/php_codesniffer": "^4.0"
@@ -55,7 +56,8 @@
5556
"autoload-dev": {
5657
"psr-4": {
5758
"Zephir\\Test\\": "tests/Zephir/",
58-
"Extension\\": "tests/Extension/"
59+
"Extension\\": "tests/Extension/",
60+
"Benchmark\\": "tests/Benchmark/"
5961
},
6062
"classmap": [
6163
"tests/fixtures/mocks/"

0 commit comments

Comments
 (0)