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
9 changes: 8 additions & 1 deletion .github/workflows/mysql-parser-extension-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ jobs:
exit( 1 );
}
'
./vendor/bin/phpunit -c ./phpunit.xml.dist tests/mysql/WP_MySQL_Lexer_Tests.php tests/parser/WP_Parser_Node_Tests.php
working-directory: packages/mysql-on-sqlite

- name: Run PHPUnit tests with parser extension
run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist
working-directory: packages/mysql-on-sqlite

sqlite-driver-extension-tests:
Expand Down Expand Up @@ -149,3 +152,7 @@ jobs:
exit( 1 );
}
'

- name: Run PHPUnit tests with SQLite driver using parser extension
run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist
working-directory: packages/mysql-on-sqlite
216 changes: 216 additions & 0 deletions .github/workflows/native-ast-perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
name: Native AST Walk Perf

on:
push:
paths:
- '.github/workflows/native-ast-perf.yml'
- 'packages/mysql-on-sqlite/src/mysql/native/**'
- 'packages/mysql-on-sqlite/tests/tools/run-native-ast-walk-benchmark.php'
- 'packages/php-ext-wp-mysql-parser/**'
pull_request:
paths:
- '.github/workflows/native-ast-perf.yml'
- 'packages/mysql-on-sqlite/src/mysql/native/**'
- 'packages/mysql-on-sqlite/tests/tools/run-native-ast-walk-benchmark.php'
- 'packages/php-ext-wp-mysql-parser/**'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
perf:
name: Native AST walk benchmark
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: none

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Install native build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libclang-dev
echo "PHP_CONFIG=$(command -v php-config)" >> "$GITHUB_ENV"
LIBCLANG_SO="$(find /usr/lib -name 'libclang.so*' | head -n 1)"
echo "LIBCLANG_PATH=$(dirname "$LIBCLANG_SO")" >> "$GITHUB_ENV"

- name: Install Composer dependencies (root)
uses: ramsey/composer-install@v3
with:
ignore-cache: "yes"
composer-options: "--optimize-autoloader"

- name: Install Composer dependencies (mysql-on-sqlite)
uses: ramsey/composer-install@v3
with:
working-directory: packages/mysql-on-sqlite
ignore-cache: "yes"
composer-options: "--optimize-autoloader"

- name: Download MySQL test query corpus
working-directory: packages/mysql-on-sqlite
run: bash tests/tools/mysql-download-tests.sh || true

- name: Build parser extension (release)
run: cargo build --release
working-directory: packages/php-ext-wp-mysql-parser

- name: Locate built extension
run: |
EXT="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so"
test -f "$EXT" || { echo "Extension not built at $EXT"; exit 1; }
echo "NATIVE_EXT=$EXT" >> "$GITHUB_ENV"

- name: Benchmark — pure-PHP path (parse only)
working-directory: packages/mysql-on-sqlite
run: |
php tests/tools/run-native-ast-walk-benchmark.php --no-walk | tee php-parse-only.txt

- name: Benchmark — pure-PHP path (walk)
working-directory: packages/mysql-on-sqlite
run: |
php tests/tools/run-native-ast-walk-benchmark.php | tee php-walk.txt

- name: Benchmark — native path (parse only)
working-directory: packages/mysql-on-sqlite
run: |
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --no-walk | tee native-parse-only.txt

- name: Benchmark — native path (walk, with identity cache)
working-directory: packages/mysql-on-sqlite
run: |
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php | tee native-walk.txt

- name: Check out baseline (PR base, no identity cache)
run: |
git fetch --no-tags --depth=1 origin codex/native-lazy-ast-facade
git worktree add ../baseline FETCH_HEAD

- name: Install Composer dependencies (baseline mysql-on-sqlite)
uses: ramsey/composer-install@v3
with:
working-directory: ../baseline/packages/mysql-on-sqlite
ignore-cache: "yes"
composer-options: "--optimize-autoloader"

- name: Build baseline parser extension (release)
run: cargo build --release
working-directory: ../baseline/packages/php-ext-wp-mysql-parser

- name: Stage benchmark + corpus into baseline
run: |
mkdir -p ../baseline/packages/mysql-on-sqlite/tests/mysql/data
cp packages/mysql-on-sqlite/tests/tools/run-native-ast-walk-benchmark.php \
../baseline/packages/mysql-on-sqlite/tests/tools/
cp packages/mysql-on-sqlite/tests/mysql/data/*.csv \
../baseline/packages/mysql-on-sqlite/tests/mysql/data/ 2>/dev/null || true

- name: Benchmark — baseline native path (walk, no identity cache)
working-directory: ../baseline/packages/mysql-on-sqlite
run: |
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php \
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-walk.txt"

- name: Benchmark — baseline native path (parse only)
working-directory: ../baseline/packages/mysql-on-sqlite
run: |
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --no-walk \
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-parse-only.txt"

# Hit-heavy scenarios — these are where the per-AST identity cache is
# supposed to win. The baseline reallocates wrappers on every accessor
# call, while the PR reuses them. Run on both to make the gap visible.
- name: Benchmark — native rewalk x10 (this PR)
working-directory: packages/mysql-on-sqlite
run: |
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=rewalk --repeat=10 \
| tee native-rewalk.txt

- name: Benchmark — baseline rewalk x10
working-directory: ../baseline/packages/mysql-on-sqlite
run: |
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=rewalk --repeat=10 \
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-rewalk.txt"

- name: Benchmark — native reread x20 (this PR)
working-directory: packages/mysql-on-sqlite
run: |
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=reread --repeat=20 \
| tee native-reread.txt

- name: Benchmark — baseline reread x20
working-directory: ../baseline/packages/mysql-on-sqlite
run: |
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=reread --repeat=20 \
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-reread.txt"

- name: Benchmark — native subtree x5 (this PR)
working-directory: packages/mysql-on-sqlite
run: |
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=subtree --repeat=5 \
| tee native-subtree.txt

- name: Benchmark — baseline subtree x5
working-directory: ../baseline/packages/mysql-on-sqlite
run: |
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=subtree --repeat=5 \
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-subtree.txt"

- name: Summarize
if: always()
working-directory: packages/mysql-on-sqlite
run: |
extract() {
# Pull a numeric field (e.g. duration=1.23s) from a benchmark
# output line. Returns "n/a" if missing.
local file="$1" key="$2"
[ -f "$file" ] || { echo "n/a"; return; }
grep -oE "${key}=[^ ]+" "$file" | head -1 | cut -d= -f2 || echo "n/a"
}

{
echo '### Native AST walk perf'
echo
echo '| scenario | result |'
echo '|---|---|'
for f in php-parse-only.txt php-walk.txt native-parse-only.txt native-walk.txt baseline-native-parse-only.txt baseline-native-walk.txt native-rewalk.txt baseline-native-rewalk.txt native-reread.txt baseline-native-reread.txt native-subtree.txt baseline-native-subtree.txt; do
[ -f "$f" ] || continue
line="$(cat "$f")"
echo "| ${f%.txt} | \`$line\` |"
done
echo
echo '### Identity-cache cost (native walk: with cache vs PR base without)'
echo
echo '| metric | with cache (this PR) | baseline | delta |'
echo '|---|---|---|---|'
for key in duration qps peak_mem walked_nodes; do
with="$(extract native-walk.txt "$key")"
base="$(extract baseline-native-walk.txt "$key")"
echo "| $key | $with | $base | — |"
done
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload raw output
if: always()
uses: actions/upload-artifact@v4
with:
name: native-ast-perf-${{ github.run_id }}
path: packages/mysql-on-sqlite/*.txt
if-no-files-found: warn
4 changes: 2 additions & 2 deletions .github/workflows/wp-tests-phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ jobs:
- name: Build and load parser extension in WordPress PHP containers
run: bash .github/workflows/wp-tests-phpunit-native-extension-setup.sh

- name: Verify WordPress uses parser extension
run: cd wordpress && node tools/local-env/scripts/docker.js run --rm php php /var/www/native-verify-extension.php
- name: Run WordPress PHPUnit tests with parser extension
run: node .github/workflows/wp-tests-phpunit-run.js

- name: Stop Docker containers
if: always()
Expand Down
1 change: 1 addition & 0 deletions packages/mysql-on-sqlite/src/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

if ( class_exists( 'WP_MySQL_Native_Parser', false ) ) {
require_once __DIR__ . '/mysql/native/mysql-rust-bridge.php';
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-ast-cache.php';
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-parser-node.php';
require_once __DIR__ . '/mysql/native/class-wp-mysql-parser.php';
} else {
Expand Down
11 changes: 11 additions & 0 deletions packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ class WP_MySQL_Parser extends WP_Parser {
*/
private $current_ast;

/**
* Reset this parser with a new token stream.
*
* @param array<WP_Parser_Token> $tokens The parser tokens.
*/
public function reset_tokens( array $tokens ): void {
$this->tokens = $tokens;
$this->position = 0;
$this->current_ast = null;
}

/**
* Parse the next query from the input SQL string.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/**
* Per-AST identity map for native parser node wrappers.
*
* The native parser extension constructs a fresh WP_MySQL_Native_Parser_Node
* every time it returns a child or descendant. Without an identity map, two
* reads of the same logical node would yield distinct PHP objects, breaking
* the WP_Parser_Node contract that callers can mutate a child in place and
* see the mutation again when they walk the tree later.
*
* One cache is created lazily on the root node and shared by reference with
* every wrapper interned through it. Lookup is keyed by the Rust-side
* `node_index`, which is stable for the lifetime of the AST.
*/
class WP_MySQL_Native_AST_Cache {
/**
* Map of native node index => WP_MySQL_Native_Parser_Node.
*
* @var array<int, WP_MySQL_Native_Parser_Node>
*/
public $nodes = array();
}
Loading
Loading