Optimize render and stream hot paths - #69
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PHPBench comparison (PHP 8.2)PHP 8.2 | Runner ubuntu24 | Base
|
PHPBench comparison (PHP 8.4)PHP 8.4 | Runner ubuntu24 | Base
|
PHPBench comparison (PHP 8.3)PHP 8.3 | Runner ubuntu24 | Base
|
PHPBench comparison (PHP 8.5)PHP 8.5 | Runner ubuntu24 | Base
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Replace static-property resolution with shared member resolution - Add coverage for cached method calls across drop instances
- Avoid unnecessary scope allocations for scalar lookups - Add regression coverage for shadowed nested values
Summary
Optimizes the render and stream hot paths. No behaviour changes, no public API removals.
The work was directed by call-count instrumentation of the storefront theme benchmark rather than by inspection — several of the biggest costs were invisible in the source. Most notably, 72% of
RenderContext::evaluate()calls were pure pass-throughs, and a generator object was being allocated for every single variable reference to deliver a value the caller almost always took the first of.Measured impact
PHPBench, base
9913b0evs79300f0, 10 iterations x 20 revs on ubuntu24:Throughput (ops/s), so positive is faster. No benchmark regressed on any version. RSD stayed at or below 2.7% on both sides, and total memory moved by less than 0.6% everywhere.
* Treat the parse and tokenize outliers on 8.4/8.5 as noise, not wins. Nothing here touches the tokenizer or the parser, and the base baseline drifted between runs on those versions (8.5 parse 298.74 → 265.04 ops/s, tokenize 917.51 → 762.32 ops/s for the same commit). The render and stream numbers are the trustworthy ones: large, consistent across all four versions, and tight RSD on both sides.
Changes
Variable resolution:
iterateVariables()generator withfindVariable()(innermost value) and the existingfindVariables()(all values). Scopes, env data and static variables are all plain arrays, so the lookup is now a barearray_key_existsinstead of a trip throughinternalContextLookup()'s dispatch. Measured in isolation, the generator alone cost ~0.079µs per lookup.findVariables()is now only used for the outer-scope fallback, i.e. when a name resolves but its lookup chain breaks on the innermost value. That fallback skips the value already walked, so a broken chain no longer repeats side effects it triggered on the way.RenderContext::evaluate()is a loop rather than recursive, and the hot call sites guard withinstanceof CanBeEvaluatedinstead of calling through for values that cannot be evaluated.normalizeValue()short-circuits on non-objects, and is skipped entirely for them.Drops and filters:
DropMetadata,DropMemberResolution,DropMemberType).Body rendering:
BodyNode::render()/stream().Textchildren — 57% of all children in the theme benchmark — append their value directly, skipping a method call, twoinstanceofchecks and the try/catch.API
RenderContext::findVariable()is new.findVariables()keeps its signature and semantics. Nothing was removed, so this is not a breaking change.Testing
vendor/bin/pest— 824 passed (2170 assertions), on PHP 8.2–8.5, prefer-lowest and prefer-stablevendor/bin/phpstan analyse --no-progress— no errorsvendor/bin/pint --test— passedNew regression coverage for the outer-scope fallback, drop member caching, and variable lookup typing.