Skip to content

Optimize render and stream hot paths - #69

Merged
cappuc merged 20 commits into
mainfrom
perf/render-stream-optimizations
Jul 30, 2026
Merged

Optimize render and stream hot paths#69
cappuc merged 20 commits into
mainfrom
perf/render-stream-optimizations

Conversation

@cappuc

@cappuc cappuc commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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 9913b0e vs 79300f0, 10 iterations x 20 revs on ubuntu24:

PHP benchRender benchStream benchParse benchTokenize
8.2 +52.3% +53.4% +1.9% +1.1%
8.3 +47.3% +51.0% +0.7% +0.6%
8.4 +39.1% +39.9% +0.3% +3.2%*
8.5 +36.7% +39.8% +4.7%* +2.7%*

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:

  • Resolve variables against the scope chain lazily instead of eagerly scanning every scope, and fast-path array scopes.
  • Replace the iterateVariables() generator with findVariable() (innermost value) and the existing findVariables() (all values). Scopes, env data and static variables are all plain arrays, so the lookup is now a bare array_key_exists instead of a trip through internalContextLookup()'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 with instanceof CanBeEvaluated instead of calling through for values that cannot be evaluated.
  • normalizeValue() short-circuits on non-objects, and is skipped entirely for them.

Drops and filters:

  • Generalize drop member resolution and cache it per class (DropMetadata, DropMemberResolution, DropMemberType).
  • Optimize filter application.

Body rendering:

  • Trim per-child overhead in BodyNode::render()/stream(). Text children — 57% of all children in the theme benchmark — append their value directly, skipping a method call, two instanceof checks 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-stable
  • vendor/bin/phpstan analyse --no-progress — no errors
  • vendor/bin/pint --test — passed
  • PHPBench regression threshold (5%) — PASSED on all four PHP versions

New regression coverage for the outer-scope fallback, drop member caching, and variable lookup typing.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PHPBench comparison (PHP 8.2)

PHP 8.2 | Runner ubuntu24 | Base 9913b0e | PR 79300f0 | 10 iterations x 20 revs | 1 warmup

Positive ops/s is faster. RSD above 5% is marked high.

Benchmark Base ops/s PR ops/s Delta ops/s RSD (base / PR) Delta memory Memory %
ThemeBench::benchParse 406.13 ops/s 413.78 ops/s +1.88% 0.97% / 1.01% +13.94 KiB +0.17%
ThemeBench::benchRender 157.38 ops/s 239.75 ops/s +52.34% 1.11% / 1.46% +8.91 KiB +0.10%
ThemeBench::benchStream 157.19 ops/s 241.07 ops/s +53.36% 1.42% / 1.68% +7.28 KiB +0.09%
ThemeBench::benchTokenize 856.32 ops/s 865.71 ops/s +1.10% 1.66% / 1.37% +23.20 KiB +0.31%
  • Improved (> +2%): 2 (median +52.85%)

  • Neutral (within ±2%): 2

  • Regressed (< -2%): 0

  • Worst throughput regression: n/a

  • Total memory change: +0.16%

  • Regression threshold (PHPBENCH_MAX_REG): 5.00%

  • Threshold status: PASSED

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PHPBench comparison (PHP 8.4)

PHP 8.4 | Runner ubuntu24 | Base 9913b0e | PR 79300f0 | 10 iterations x 20 revs | 1 warmup

Positive ops/s is faster. RSD above 5% is marked high.

Benchmark Base ops/s PR ops/s Delta ops/s RSD (base / PR) Delta memory Memory %
ThemeBench::benchParse 292.89 ops/s 293.81 ops/s +0.31% 1.36% / 2.15% -3.59 KiB -0.04%
ThemeBench::benchRender 121.52 ops/s 169.04 ops/s +39.10% 1.36% / 1.23% -26.05 KiB -0.30%
ThemeBench::benchStream 122.94 ops/s 172.01 ops/s +39.91% 1.49% / 1.79% -61.44 KiB -0.77%
ThemeBench::benchTokenize 882.37 ops/s 910.70 ops/s +3.21% 1.91% / 2.09% +260.94 KiB +3.49%
  • Improved (> +2%): 3 (median +39.10%)

  • Neutral (within ±2%): 1

  • Regressed (< -2%): 0

  • Worst throughput regression: n/a

  • Total memory change: +0.52%

  • Regression threshold (PHPBENCH_MAX_REG): 5.00%

  • Threshold status: PASSED

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PHPBench comparison (PHP 8.3)

PHP 8.3 | Runner ubuntu24 | Base 9913b0e | PR 79300f0 | 10 iterations x 20 revs | 1 warmup

Positive ops/s is faster. RSD above 5% is marked high.

Benchmark Base ops/s PR ops/s Delta ops/s RSD (base / PR) Delta memory Memory %
ThemeBench::benchParse 400.11 ops/s 402.88 ops/s +0.69% 2.51% / 0.87% -3.73 KiB -0.04%
ThemeBench::benchRender 159.98 ops/s 235.69 ops/s +47.32% 0.57% / 1.17% +8.91 KiB +0.10%
ThemeBench::benchStream 158.81 ops/s 239.83 ops/s +51.02% 1.04% / 2.05% +7.28 KiB +0.09%
ThemeBench::benchTokenize 847.54 ops/s 852.73 ops/s +0.61% 1.36% / 1.49% -3.57 KiB -0.05%
  • Improved (> +2%): 2 (median +49.17%)

  • Neutral (within ±2%): 2

  • Regressed (< -2%): 0

  • Worst throughput regression: n/a

  • Total memory change: +0.03%

  • Regression threshold (PHPBENCH_MAX_REG): 5.00%

  • Threshold status: PASSED

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PHPBench comparison (PHP 8.5)

PHP 8.5 | Runner ubuntu24 | Base 9913b0e | PR 79300f0 | 10 iterations x 20 revs | 1 warmup

Positive ops/s is faster. RSD above 5% is marked high.

Benchmark Base ops/s PR ops/s Delta ops/s RSD (base / PR) Delta memory Memory %
ThemeBench::benchParse 265.04 ops/s 277.59 ops/s +4.73% 1.50% / 2.66% -309.91 KiB -3.42%
ThemeBench::benchRender 122.67 ops/s 167.63 ops/s +36.66% 1.14% / 1.28% +110.45 KiB +1.25%
ThemeBench::benchStream 122.80 ops/s 171.64 ops/s +39.78% 1.53% / 1.36% +156.47 KiB +1.98%
ThemeBench::benchTokenize 762.32 ops/s 783.22 ops/s +2.74% 2.05% / 2.34% -3.57 KiB -0.05%
  • Improved (> +2%): 4 (median +20.70%)

  • Neutral (within ±2%): 0

  • Regressed (< -2%): 0

  • Worst throughput regression: n/a

  • Total memory change: -0.14%

  • Regression threshold (PHPBENCH_MAX_REG): 5.00%

  • Threshold status: PASSED

@cappuc
cappuc merged commit 60b7dbe into main Jul 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant