Skip to content

Commit 2c07dac

Browse files
committed
Add hit-heavy perf scenarios to compare cache vs baseline
The walk benchmark we already had is cache-miss heavy (one walk per AST, every node visited once), so the identity cache shows up there as a small overhead rather than a win. The cache is supposed to pay back in hit-heavy patterns: re-walks of the same tree, repeated child reads at the root, and translator-style passes that re-enter visited subtrees. Adds three modes (--mode=rewalk|reread|subtree) and runs each on both the PR and the baseline so the comparison is apples-to-apples on the same runner, same corpus.
1 parent 6c94572 commit 2c07dac

2 files changed

Lines changed: 143 additions & 30 deletions

File tree

.github/workflows/native-ast-perf.yml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,48 @@ jobs:
131131
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --no-walk \
132132
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-parse-only.txt"
133133
134+
# Hit-heavy scenarios — these are where the per-AST identity cache is
135+
# supposed to win. The baseline reallocates wrappers on every accessor
136+
# call, while the PR reuses them. Run on both to make the gap visible.
137+
- name: Benchmark — native rewalk x10 (this PR)
138+
working-directory: packages/mysql-on-sqlite
139+
run: |
140+
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=rewalk --repeat=10 \
141+
| tee native-rewalk.txt
142+
143+
- name: Benchmark — baseline rewalk x10
144+
working-directory: ../baseline/packages/mysql-on-sqlite
145+
run: |
146+
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
147+
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=rewalk --repeat=10 \
148+
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-rewalk.txt"
149+
150+
- name: Benchmark — native reread x20 (this PR)
151+
working-directory: packages/mysql-on-sqlite
152+
run: |
153+
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=reread --repeat=20 \
154+
| tee native-reread.txt
155+
156+
- name: Benchmark — baseline reread x20
157+
working-directory: ../baseline/packages/mysql-on-sqlite
158+
run: |
159+
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
160+
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=reread --repeat=20 \
161+
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-reread.txt"
162+
163+
- name: Benchmark — native subtree x5 (this PR)
164+
working-directory: packages/mysql-on-sqlite
165+
run: |
166+
php -d extension="$NATIVE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=subtree --repeat=5 \
167+
| tee native-subtree.txt
168+
169+
- name: Benchmark — baseline subtree x5
170+
working-directory: ../baseline/packages/mysql-on-sqlite
171+
run: |
172+
BASE_EXT="$(realpath ../../packages/php-ext-wp-mysql-parser/target/release/libwp_mysql_parser.so)"
173+
php -d extension="$BASE_EXT" tests/tools/run-native-ast-walk-benchmark.php --mode=subtree --repeat=5 \
174+
| tee "$GITHUB_WORKSPACE/packages/mysql-on-sqlite/baseline-native-subtree.txt"
175+
134176
- name: Summarize
135177
if: always()
136178
working-directory: packages/mysql-on-sqlite
@@ -148,7 +190,7 @@ jobs:
148190
echo
149191
echo '| scenario | result |'
150192
echo '|---|---|'
151-
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; do
193+
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
152194
[ -f "$f" ] || continue
153195
line="$(cat "$f")"
154196
echo "| ${f%.txt} | \`$line\` |"

packages/mysql-on-sqlite/tests/tools/run-native-ast-walk-benchmark.php

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@
33
/**
44
* Benchmark for the native AST walk path with the per-AST identity cache.
55
*
6-
* Parses every query in the MySQL server suite, then walks each AST
7-
* exhaustively through `get_descendants()` and `get_first_child_node()`
8-
* loops to exercise the bridge accessors and the identity map. Reports
9-
* wall time, peak memory, and a basic identity-stability check so the
10-
* cache cost can be compared against the no-cache baseline.
6+
* Parses every query in the MySQL server suite, then walks each AST through
7+
* a configurable mode to exercise the bridge accessors and the identity map.
8+
* Reports wall time, peak memory, and a basic identity-stability check so
9+
* the cache cost can be compared against the no-cache baseline.
10+
*
11+
* Modes:
12+
* walk — single full descendant walk per AST (cache-miss heavy).
13+
* no-walk — parse only.
14+
* rewalk=N — repeat the descendant walk N times per AST (1st pass is
15+
* misses, remaining passes are all hits — the scenario the
16+
* identity cache is supposed to win on).
17+
* reread=N — for each top-level child node, call accessors N times to
18+
* exercise repeated-read hit paths.
19+
* subtree=N — walk descendants once, then re-read each one's first child
20+
* N times — models translator/rewriter passes that re-enter
21+
* the same subtrees.
1122
*
1223
* Usage:
13-
* php run-native-ast-walk-benchmark.php # walks via accessors
14-
* php run-native-ast-walk-benchmark.php --no-walk # parse only, baseline
24+
* php run-native-ast-walk-benchmark.php
25+
* php run-native-ast-walk-benchmark.php --mode=no-walk
26+
* php run-native-ast-walk-benchmark.php --mode=rewalk --repeat=10
27+
* php run-native-ast-walk-benchmark.php --mode=reread --repeat=10
28+
* php run-native-ast-walk-benchmark.php --mode=subtree --repeat=5
1529
*
1630
* The script auto-detects the native extension. Without it, the walk
1731
* exercises the pure-PHP WP_Parser_Node path, which is useful as the
@@ -26,7 +40,17 @@ function ( $severity, $message, $file, $line ) {
2640

2741
require_once __DIR__ . '/../../src/load.php';
2842

29-
$walk_tree = ! in_array( '--no-walk', $argv, true );
43+
$mode = 'walk';
44+
$repeat = 1;
45+
foreach ( $argv as $arg ) {
46+
if ( '--no-walk' === $arg ) {
47+
$mode = 'no-walk';
48+
} elseif ( 0 === strpos( $arg, '--mode=' ) ) {
49+
$mode = substr( $arg, 7 );
50+
} elseif ( 0 === strpos( $arg, '--repeat=' ) ) {
51+
$repeat = max( 1, (int) substr( $arg, 9 ) );
52+
}
53+
}
3054

3155
$grammar_data = include __DIR__ . '/../../src/mysql/mysql-grammar.php';
3256
$grammar = new WP_Parser_Grammar( $grammar_data );
@@ -68,24 +92,70 @@ function ( $severity, $message, $file, $line ) {
6892
}
6993
++$total;
7094

71-
if ( ! $walk_tree ) {
72-
continue;
73-
}
74-
75-
// Exhaustive descendant walk — exercises both the per-call accessor
76-
// path and (when the native extension is loaded) the identity map.
77-
$descendants = $ast->get_descendants();
78-
$walked += count( $descendants );
79-
80-
// Re-read the first child a few times and confirm identity is
81-
// stable. With the cache, this must be the same instance every
82-
// call; a regression would surface as a cheap, deterministic flag.
83-
$first = $ast->get_first_child_node();
84-
if ( null !== $first ) {
85-
$again = $ast->get_first_child_node();
86-
if ( $first !== $again ) {
87-
$identity_ok = false;
88-
}
95+
switch ( $mode ) {
96+
case 'no-walk':
97+
break;
98+
99+
case 'walk':
100+
$descendants = $ast->get_descendants();
101+
$walked += count( $descendants );
102+
103+
$first = $ast->get_first_child_node();
104+
if ( null !== $first ) {
105+
$again = $ast->get_first_child_node();
106+
if ( $first !== $again ) {
107+
$identity_ok = false;
108+
}
109+
}
110+
break;
111+
112+
case 'rewalk':
113+
// Repeated full-tree walks. After the first pass every wrapper
114+
// the cache returns is a hit; without the cache, every pass
115+
// re-allocates wrappers for the entire tree from scratch.
116+
for ( $r = 0; $r < $repeat; $r++ ) {
117+
$descendants = $ast->get_descendants();
118+
$walked += count( $descendants );
119+
}
120+
break;
121+
122+
case 'reread':
123+
// Repeated top-level child reads. Models analysis passes that
124+
// keep poking at the root of the tree.
125+
for ( $r = 0; $r < $repeat; $r++ ) {
126+
$child = $ast->get_first_child_node();
127+
if ( null !== $child ) {
128+
++$walked;
129+
// Identity must hold across repeated reads.
130+
if ( $r > 0 && $child !== $prev ) {
131+
$identity_ok = false;
132+
}
133+
$prev = $child;
134+
}
135+
}
136+
break;
137+
138+
case 'subtree':
139+
// Walk descendants once, then for each descendant re-read its
140+
// first child N times. Models translator/rewriter passes that
141+
// re-enter previously visited subtrees.
142+
$descendants = $ast->get_descendants();
143+
foreach ( $descendants as $d ) {
144+
if ( ! $d instanceof WP_Parser_Node ) {
145+
continue;
146+
}
147+
for ( $r = 0; $r < $repeat; $r++ ) {
148+
$child = $d->get_first_child_node();
149+
if ( null !== $child ) {
150+
++$walked;
151+
}
152+
}
153+
}
154+
break;
155+
156+
default:
157+
fwrite( STDERR, "Unknown mode: $mode\n" );
158+
exit( 2 );
89159
}
90160
} catch ( Throwable $e ) {
91161
++$failures;
@@ -97,9 +167,10 @@ function ( $severity, $message, $file, $line ) {
97167
$native = class_exists( 'WP_MySQL_Native_Parser', false ) ? 'native' : 'php';
98168

99169
printf(
100-
"path=%s walk=%s parsed=%d walked_nodes=%d failures=%d duration=%.4fs qps=%d peak_mem=%.1fMB identity_ok=%s\n",
170+
"path=%s mode=%s repeat=%d parsed=%d walked_nodes=%d failures=%d duration=%.4fs qps=%d peak_mem=%.1fMB identity_ok=%s\n",
101171
$native,
102-
$walk_tree ? 'yes' : 'no',
172+
$mode,
173+
$repeat,
103174
$total,
104175
$walked,
105176
$failures,
@@ -110,6 +181,6 @@ function ( $severity, $message, $file, $line ) {
110181
);
111182

112183
if ( ! $identity_ok ) {
113-
fwrite( STDERR, "Identity check failed: get_first_child_node() returned different instances.\n" );
184+
fwrite( STDERR, "Identity check failed: accessor returned different instances on repeat read.\n" );
114185
exit( 1 );
115186
}

0 commit comments

Comments
 (0)