Skip to content

Commit 1a7cead

Browse files
committed
Move identity cache from PHP into the Rust extension
The PHP-side cache from #391 fixed the correctness regression but added 27-31% to translator-style re-entry workloads — every accessor probes a PHP zend_array that, at full-walk scale, holds 4.8M entries, and PHP still allocates a fresh wrapper before the cache check gets a chance to drop it. This moves the cache to where it can actually skip work. WP_MySQL_Native_Ast now carries a RefCell<HashMap<usize, ZBox<ZendObject>>>; cached_node_zval returns a Zval pointing at the stored wrapper with refcount bumped on a hit, so the allocation and four zend_update_property calls of the construction path are gone. Every accessor (get_first_child_node, get_descendants, etc.) routes through this helper. PHP-side cache disappears: WP_MySQL_Native_Parser_Node goes back to plain bridge calls, the WP_MySQL_Native_AST_Cache holder is removed. Mutation semantics are unchanged — materialize_native_children still flips was_mutated and copies the same wrappers (now interned by Rust) into $this->children, so any caller mutation made before append_child still survives. Tokens are not yet interned. The public token API has no mutators and no caller in this repo relies on token identity; if that changes we extend node_cache with a token map.
1 parent 2c07dac commit 1a7cead

4 files changed

Lines changed: 136 additions & 221 deletions

File tree

packages/mysql-on-sqlite/src/load.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
if ( class_exists( 'WP_MySQL_Native_Parser', false ) ) {
2929
require_once __DIR__ . '/mysql/native/mysql-rust-bridge.php';
30-
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-ast-cache.php';
3130
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-parser-node.php';
3231
require_once __DIR__ . '/mysql/native/class-wp-mysql-parser.php';
3332
} else {

packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-ast-cache.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php

Lines changed: 8 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,19 @@
88
* caller actually walks the tree. On the first mutation (append_child or
99
* merge_fragment), the node materializes its children into the inherited
1010
* `$children` array and behaves like a plain WP_Parser_Node from then on.
11-
*
12-
* Wrappers returned by accessors are interned through a per-AST identity
13-
* map (WP_MySQL_Native_AST_Cache) so two reads of the same logical node
14-
* yield the same PHP instance. This preserves the WP_Parser_Node contract
15-
* that mutations performed on a child via `get_first_child_node()` remain
16-
* visible when the same child is reached again, including after the parent
17-
* has materialized.
1811
*/
1912
class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
2013
private $native_ast = null;
2114
private $native_node_index = null;
2215
private $was_mutated = false;
2316

24-
/**
25-
* Per-AST identity map shared between every interned wrapper.
26-
*
27-
* Created lazily on the first child access; the root wrapper is the
28-
* first entry. Children inherit the same cache instance by reference.
29-
*
30-
* @var WP_MySQL_Native_AST_Cache|null
31-
*/
32-
private $cache = null;
33-
3417
public function __construct( $rule_id, $rule_name, $native_ast = null, $native_node_index = null ) {
3518
parent::__construct( $rule_id, $rule_name );
3619

3720
$this->native_ast = $native_ast;
3821
$this->native_node_index = $native_node_index;
3922
}
4023

41-
/**
42-
* Native node index in the Rust-owned arena.
43-
*
44-
* Exposed so the identity cache can key on it. Returns null after
45-
* the wrapper has materialized — at that point the node is detached
46-
* from the native arena and behaves like a plain WP_Parser_Node.
47-
*
48-
* @return int|null
49-
*/
50-
public function get_native_node_index(): ?int {
51-
return $this->native_node_index;
52-
}
53-
5424
/** @inheritDoc */
5525
public function append_child( $node ) {
5626
$this->materialize_native_children();
@@ -95,15 +65,15 @@ public function get_first_child() {
9565
if ( $this->was_mutated() ) {
9666
return parent::get_first_child();
9767
}
98-
return $this->intern( wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index ) );
68+
return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index );
9969
}
10070

10171
/** @inheritDoc */
10272
public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
10373
if ( $this->was_mutated() ) {
10474
return parent::get_first_child_node( $rule_name );
10575
}
106-
return $this->intern( wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name ) );
76+
return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name );
10777
}
10878

10979
/** @inheritDoc */
@@ -119,7 +89,7 @@ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Pars
11989
if ( $this->was_mutated() ) {
12090
return parent::get_first_descendant_node( $rule_name );
12191
}
122-
return $this->intern( wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name ) );
92+
return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name );
12393
}
12494

12595
/** @inheritDoc */
@@ -135,15 +105,15 @@ public function get_children(): array {
135105
if ( $this->was_mutated() ) {
136106
return parent::get_children();
137107
}
138-
return $this->intern_all( wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ) );
108+
return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index );
139109
}
140110

141111
/** @inheritDoc */
142112
public function get_child_nodes( ?string $rule_name = null ): array {
143113
if ( $this->was_mutated() ) {
144114
return parent::get_child_nodes( $rule_name );
145115
}
146-
return $this->intern_nodes( wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name ) );
116+
return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name );
147117
}
148118

149119
/** @inheritDoc */
@@ -159,15 +129,15 @@ public function get_descendants(): array {
159129
if ( $this->was_mutated() ) {
160130
return parent::get_descendants();
161131
}
162-
return $this->intern_all( wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index ) );
132+
return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index );
163133
}
164134

165135
/** @inheritDoc */
166136
public function get_descendant_nodes( ?string $rule_name = null ): array {
167137
if ( $this->was_mutated() ) {
168138
return parent::get_descendant_nodes( $rule_name );
169139
}
170-
return $this->intern_nodes( wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name ) );
140+
return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name );
171141
}
172142

173143
/** @inheritDoc */
@@ -198,128 +168,12 @@ private function was_mutated(): bool {
198168
return $this->was_mutated;
199169
}
200170

201-
/**
202-
* Intern a single accessor return value through the per-AST cache.
203-
*
204-
* Tokens and nulls pass through untouched. Native node wrappers are
205-
* keyed on their `native_node_index`: on cache miss, the freshly
206-
* constructed wrapper is stored and given the cache reference; on
207-
* cache hit, the canonical instance is returned and the new wrapper
208-
* is discarded so callers see stable identity and surviving mutations.
209-
*
210-
* @param mixed $value Return value from the Rust bridge.
211-
* @return mixed
212-
*/
213-
private function intern( $value ) {
214-
if ( ! $value instanceof WP_MySQL_Native_Parser_Node ) {
215-
return $value;
216-
}
217-
218-
$cache = $this->ensure_cache();
219-
$index = $value->native_node_index;
220-
if ( null === $index ) {
221-
return $value;
222-
}
223-
if ( isset( $cache->nodes[ $index ] ) ) {
224-
return $cache->nodes[ $index ];
225-
}
226-
$value->cache = $cache;
227-
$cache->nodes[ $index ] = $value;
228-
return $value;
229-
}
230-
231-
/**
232-
* Intern every entry in an accessor return array.
233-
*
234-
* Hot path: this runs once per descendant when a caller walks the tree,
235-
* so cache lookup and the cache-miss write are inlined and the cache
236-
* reference is hoisted out of the loop.
237-
*
238-
* @param array $values
239-
* @return array
240-
*/
241-
private function intern_all( array $values ): array {
242-
if ( ! $values ) {
243-
return $values;
244-
}
245-
$cache = $this->cache ?? $this->ensure_cache();
246-
$nodes = &$cache->nodes;
247-
foreach ( $values as $i => $value ) {
248-
if ( ! $value instanceof WP_MySQL_Native_Parser_Node ) {
249-
continue;
250-
}
251-
$index = $value->native_node_index;
252-
if ( null === $index ) {
253-
continue;
254-
}
255-
if ( isset( $nodes[ $index ] ) ) {
256-
$values[ $i ] = $nodes[ $index ];
257-
} else {
258-
$value->cache = $cache;
259-
$nodes[ $index ] = $value;
260-
}
261-
}
262-
return $values;
263-
}
264-
265-
/**
266-
* Intern array of guaranteed-node results (no token/null mixing).
267-
*
268-
* Used by `get_child_nodes()` / `get_descendant_nodes()` whose Rust
269-
* bridge returns only WP_MySQL_Native_Parser_Node instances. Skips
270-
* the per-item `instanceof` check that intern_all() must do for the
271-
* mixed `get_children()` / `get_descendants()` arrays.
272-
*
273-
* @param array $values
274-
* @return array
275-
*/
276-
private function intern_nodes( array $values ): array {
277-
if ( ! $values ) {
278-
return $values;
279-
}
280-
$cache = $this->cache ?? $this->ensure_cache();
281-
$nodes = &$cache->nodes;
282-
foreach ( $values as $i => $value ) {
283-
$index = $value->native_node_index;
284-
if ( isset( $nodes[ $index ] ) ) {
285-
$values[ $i ] = $nodes[ $index ];
286-
} else {
287-
$value->cache = $cache;
288-
$nodes[ $index ] = $value;
289-
}
290-
}
291-
return $values;
292-
}
293-
294-
/**
295-
* Lazily build (or reuse) the per-AST identity map.
296-
*
297-
* The root wrapper is constructed without a cache, so the first time
298-
* any accessor needs to intern a child, it creates the cache and
299-
* registers itself as the root entry. Subsequent interns on this
300-
* wrapper or any descendant share the same cache by reference.
301-
*
302-
* @return WP_MySQL_Native_AST_Cache
303-
*/
304-
private function ensure_cache(): WP_MySQL_Native_AST_Cache {
305-
if ( null === $this->cache ) {
306-
$this->cache = new WP_MySQL_Native_AST_Cache();
307-
if ( null !== $this->native_node_index ) {
308-
$this->cache->nodes[ $this->native_node_index ] = $this;
309-
}
310-
}
311-
return $this->cache;
312-
}
313-
314171
private function materialize_native_children(): void {
315172
if ( $this->was_mutated ) {
316173
return;
317174
}
318175

319-
// Pull children through the cache so any wrapper a caller already
320-
// mutated via get_first_child_node() etc. survives the transition
321-
// into $this->children — same instance, same mutations.
322-
$this->children = $this->intern_all( wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ) );
176+
$this->children = wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index );
323177
$this->native_ast = null;
324178
$this->native_node_index = null;
325179
$this->was_mutated = true;

0 commit comments

Comments
 (0)