From 24e3529bd26503807794b5015482e76edaa7af9e Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Thu, 30 Jul 2026 09:43:26 +0200 Subject: [PATCH 01/11] Revert "removed FilterBench" This reverts commit 1a7ae9133bd0c8bf9c0c06bdfa0993226a145ba6. --- performance/benchmarks/FilterBench.php | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 performance/benchmarks/FilterBench.php diff --git a/performance/benchmarks/FilterBench.php b/performance/benchmarks/FilterBench.php new file mode 100644 index 0000000..38270a3 --- /dev/null +++ b/performance/benchmarks/FilterBench.php @@ -0,0 +1,53 @@ +environment = EnvironmentFactory::new()->build(); + $this->noArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | upcase | escape }}', 32)); + $this->argumentsTemplate = $this->environment->parseString(str_repeat('{{ value | append: suffix | replace: from, to }}', 32)); + } + + public function benchNoArguments(): void + { + $this->noArgumentsTemplate->render($this->context()); + } + + public function benchArguments(): void + { + $this->argumentsTemplate->render($this->context()); + } + + private function context(): \Keepsuit\Liquid\Render\RenderContext + { + return $this->environment->newRenderContext(staticData: [ + 'value' => 'example', + 'suffix' => '-suffix', + 'from' => 'example', + 'to' => 'value', + ]); + } +} From 6811cb3cc3e9bb9b7a385803cae6fe8bf24fbdfc Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Thu, 30 Jul 2026 10:39:56 +0200 Subject: [PATCH 02/11] Revamp benchmark suite --- .github/workflows/phpbench.yml | 9 +- composer.json | 3 +- performance/ProfileReport.php | 55 ++++++ performance/benchmarks/FilterBench.php | 53 ------ performance/benchmarks/LiquidBench.php | 70 -------- .../{RenderBench.php => OperationBench.php} | 30 +++- .../Support/ComplexThemeFixture.php | 126 +++++++++++++ performance/benchmarks/TemplateCacheBench.php | 167 +++++++++++------- performance/benchmarks/ThemeBench.php | 63 +++++++ performance/profile-theme.php | 62 +++++++ .../Performance/ComplexThemeFixtureTest.php | 23 +++ tests/Unit/Performance/ProfileReportTest.php | 43 +++++ tools/phpbench-compare.php | 4 +- 13 files changed, 510 insertions(+), 198 deletions(-) create mode 100644 performance/ProfileReport.php delete mode 100644 performance/benchmarks/FilterBench.php delete mode 100644 performance/benchmarks/LiquidBench.php rename performance/benchmarks/{RenderBench.php => OperationBench.php} (70%) create mode 100644 performance/benchmarks/Support/ComplexThemeFixture.php create mode 100644 performance/benchmarks/ThemeBench.php create mode 100644 performance/profile-theme.php create mode 100644 tests/Integration/Performance/ComplexThemeFixtureTest.php create mode 100644 tests/Unit/Performance/ProfileReportTest.php diff --git a/.github/workflows/phpbench.yml b/.github/workflows/phpbench.yml index 78344a7..b61d731 100644 --- a/.github/workflows/phpbench.yml +++ b/.github/workflows/phpbench.yml @@ -14,11 +14,11 @@ permissions: env: REQUIRED_PHP_EXTENSIONS: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo - # TemplateCacheBench::benchVarExporter segfaults on CI for PHP 8.4/8.5. Exclude it on + # TemplateCacheBench::benchLoadAndRenderVarExporter segfaults on CI for PHP 8.4/8.5. Exclude it on # every version, not just those two: a benchmark set that varies per PHP version makes # the per-version comparison summaries incomparable with each other. Local runs are # unfiltered, so the benchmark still exists — it just does not gate PRs. - PHPBENCH_FILTER: '^(?!.*TemplateCacheBench::benchVarExporter$).*' + PHPBENCH_FILTER: '^(?!.*TemplateCacheBench::benchLoadAndRenderVarExporter$).*' jobs: benchmark: @@ -78,9 +78,9 @@ jobs: set -euo pipefail vendor/bin/phpbench run \ + --group=macro \ --filter="$PHPBENCH_FILTER" \ --progress=none \ - --warmup=1 \ --retry-threshold=5 \ --report=aggregate \ --output=json > "$WORKSPACE/pr.json" @@ -93,7 +93,6 @@ jobs: vendor/bin/phpbench run \ --filter="$PHPBENCH_FILTER" \ --progress=none \ - --warmup=1 \ --retry-threshold=5 \ --report=aggregate \ --output=json > "$WORKSPACE/base.json" @@ -106,7 +105,7 @@ jobs: PHPBENCH_PHP_VERSION: ${{ matrix.php }} PHPBENCH_BASE_SHA: ${{ github.event.pull_request.base.sha }} PHPBENCH_PR_SHA: ${{ github.event.pull_request.head.sha }} - PHPBENCH_WARMUP: '1' + PHPBENCH_WARMUP: '0' run: | set +e export PHPBENCH_RUNNER="${ImageOS:-$RUNNER_OS}" diff --git a/composer.json b/composer.json index 90ca7c8..2bb7f99 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,8 @@ "pint", "phpstan analyse" ], - "benchmark": "phpbench run --report=aggregate", + "benchmark": "phpbench run --group=macro --report=aggregate", + "benchmark:micro": "phpbench run --group=micro --report=aggregate", "profile": "phpbench xdebug:profile" }, "config": { diff --git a/performance/ProfileReport.php b/performance/ProfileReport.php new file mode 100644 index 0000000..ef3bbdc --- /dev/null +++ b/performance/ProfileReport.php @@ -0,0 +1,55 @@ +> + * } + */ + public static function fromProfiler(Profiler $profiler): array + { + return [ + 'schema_version' => 1, + 'generated_at' => date(DATE_ATOM), + 'php_version' => PHP_VERSION, + 'profiles' => array_map(self::profile(...), $profiler->getProfiles()), + ]; + } + + /** + * @return array{ + * type: string, + * name: string, + * duration: float, + * self_duration: float, + * memory_usage: int, + * peak_memory_usage: int, + * children: array> + * } + */ + private static function profile(Profile $profile): array + { + return [ + 'type' => $profile->type->value, + 'name' => $profile->name, + 'duration' => $profile->getDuration(), + 'self_duration' => $profile->getSelfDuration(), + 'memory_usage' => $profile->getMemoryUsage(), + 'peak_memory_usage' => $profile->getPeakMemoryUsage(), + 'children' => array_map(self::profile(...), $profile->getChildren()), + ]; + } +} diff --git a/performance/benchmarks/FilterBench.php b/performance/benchmarks/FilterBench.php deleted file mode 100644 index 38270a3..0000000 --- a/performance/benchmarks/FilterBench.php +++ /dev/null @@ -1,53 +0,0 @@ -environment = EnvironmentFactory::new()->build(); - $this->noArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | upcase | escape }}', 32)); - $this->argumentsTemplate = $this->environment->parseString(str_repeat('{{ value | append: suffix | replace: from, to }}', 32)); - } - - public function benchNoArguments(): void - { - $this->noArgumentsTemplate->render($this->context()); - } - - public function benchArguments(): void - { - $this->argumentsTemplate->render($this->context()); - } - - private function context(): \Keepsuit\Liquid\Render\RenderContext - { - return $this->environment->newRenderContext(staticData: [ - 'value' => 'example', - 'suffix' => '-suffix', - 'from' => 'example', - 'to' => 'value', - ]); - } -} diff --git a/performance/benchmarks/LiquidBench.php b/performance/benchmarks/LiquidBench.php deleted file mode 100644 index 53719ce..0000000 --- a/performance/benchmarks/LiquidBench.php +++ /dev/null @@ -1,70 +0,0 @@ -themeRunner = $this->getThemeRunner(); - } - - public function benchTokenize(): void - { - $this->themeRunner->tokenize(); - } - - public function benchParsing(): void - { - $this->themeRunner->compile(); - } - - public function benchRender(): void - { - $this->themeRunner->render(); - } - - public function benchStream(): void - { - $this->themeRunner->stream(); - } - - public function benchParsingAndRendering(): void - { - $this->themeRunner->run(); - } - - public function benchParsingAndStreaming(): void - { - $this->themeRunner->runStreaming(); - } - - protected function getThemeRunner(): ThemeRunner - { - $environment = EnvironmentFactory::new() - ->registerTag(CommentFormTag::class) - ->registerTag(PaginateTag::class) - ->registerFilters(CustomFilters::class) - ->build(); - - return new ThemeRunner($environment); - } -} diff --git a/performance/benchmarks/RenderBench.php b/performance/benchmarks/OperationBench.php similarity index 70% rename from performance/benchmarks/RenderBench.php rename to performance/benchmarks/OperationBench.php index edc819e..fa2b8d0 100644 --- a/performance/benchmarks/RenderBench.php +++ b/performance/benchmarks/OperationBench.php @@ -7,17 +7,19 @@ use Keepsuit\Liquid\Performance\Shopify\DatabaseDrop; use Keepsuit\Liquid\Template; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; use PhpBench\Attributes\OutputMode; use PhpBench\Attributes\OutputTimeUnit; use PhpBench\Attributes\Revs; +#[Groups(['micro'])] #[Iterations(10)] #[Revs(20)] #[OutputMode('throughput')] #[OutputTimeUnit('seconds', precision: 3)] #[BeforeMethods('setUp')] -class RenderBench +class OperationBench { private Environment $environment; @@ -25,11 +27,17 @@ class RenderBench private Template $nestedTemplate; + private Template $filterWithoutArgumentsTemplate; + + private Template $filterWithArgumentsTemplate; + public function setUp(): void { $this->environment = EnvironmentFactory::new()->build(); $this->scalarTemplate = $this->environment->parseString(str_repeat('{{ value }}', 64)); $this->nestedTemplate = $this->environment->parseString(str_repeat('{{ product.title }}', 64)); + $this->filterWithoutArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | upcase | escape }}', 32)); + $this->filterWithArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | append: suffix | replace: from, to }}', 32)); } public function benchScalarRender(): void @@ -74,6 +82,16 @@ public function benchDropStream(): void ))); } + public function benchFilterWithoutArguments(): void + { + $this->filterWithoutArgumentsTemplate->render($this->filterContext()); + } + + public function benchFilterWithArguments(): void + { + $this->filterWithArgumentsTemplate->render($this->filterContext()); + } + /** * @param \Generator $stream */ @@ -83,4 +101,14 @@ private function drain(\Generator $stream): void $stream->next(); } } + + private function filterContext(): \Keepsuit\Liquid\Render\RenderContext + { + return $this->environment->newRenderContext(staticData: [ + 'value' => 'example', + 'suffix' => '-suffix', + 'from' => 'example', + 'to' => 'value', + ]); + } } diff --git a/performance/benchmarks/Support/ComplexThemeFixture.php b/performance/benchmarks/Support/ComplexThemeFixture.php new file mode 100644 index 0000000..ade0a3b --- /dev/null +++ b/performance/benchmarks/Support/ComplexThemeFixture.php @@ -0,0 +1,126 @@ +setFilesystem(new ComplexThemeFileSystem(self::templateSources())) + ->registerFilters(ComplexThemeFilters::class); + + if ($templatesCache !== null) { + $factory->setTemplatesCache($templatesCache); + } + + return $factory->build(); + } + + public static function rootTemplateName(): string + { + return self::ROOT_TEMPLATE_NAME; + } + + public static function rootTemplateSource(): string + { + return self::templateSources()[self::ROOT_TEMPLATE_NAME]; + } + + /** + * @return array + */ + public static function templateSources(): array + { + return [ + self::ROOT_TEMPLATE_NAME => '{% render "page-shell", collection: collection %}', + 'page-shell' => "
\n {% render \"collection-section\", collection: collection %}\n
", + 'collection-section' => "

{{ collection.title }}

{{ collection.products | size }} products

\n
\n{% for product in collection.products %} {% render \"product-card\", product: product, label: collection.label %}\n{% endfor %}
", + 'product-card' => '

{{ product.title }}

{{ product.vendor | upcase }}

{{ product.price_cents | fixture_money }}

{{ label }}

{{ product.title }} / {{ product.vendor }} / {{ product.inventory_label }} | {{ product.title }} / {{ product.vendor }} / {{ product.inventory_label }}

', + ]; + } + + /** + * @return array + */ + public static function renderData(): array + { + return [ + 'collection' => [ + 'handle' => 'summer-essentials', + 'title' => 'Summer Essentials', + 'label' => 'summer', + 'products' => [ + new ComplexThemeProduct('linen-shirt', 'Linen Shirt', 'ACME Apparel', 3950), + new ComplexThemeProduct('canvas-tote', 'Canvas Tote', 'Field Goods', 2400), + new ComplexThemeProduct('sun-hat', 'Sun Hat', 'Coastline', 1875), + ], + ], + ]; + } + + public static function newRenderContext(Environment $environment): RenderContext + { + return $environment->newRenderContext(staticData: self::renderData()); + } +} + +final class ComplexThemeFileSystem implements LiquidFileSystem +{ + /** + * @param array $templates + */ + public function __construct( + private readonly array $templates, + ) {} + + public function readTemplateFile(string $templateName): string + { + return $this->templates[$templateName] ?? throw new \RuntimeException("Unknown fixture template [$templateName]."); + } +} + +final class ComplexThemeFilters extends FiltersProvider +{ + public function fixtureMoney(int|float $cents): string + { + return '€'.number_format($cents / 100, 2, '.', ''); + } +} + +final class ComplexThemeProduct extends Drop +{ + public function __construct( + public readonly string $handle, + public readonly string $title, + private readonly string $vendorName, + private readonly int $priceCents, + ) {} + + public function vendor(): string + { + return $this->vendorName; + } + + public function priceCents(): int + { + return $this->priceCents; + } + + #[Cache] + public function inventoryLabel(): int + { + return $this->priceCents; + } +} diff --git a/performance/benchmarks/TemplateCacheBench.php b/performance/benchmarks/TemplateCacheBench.php index fc2e348..a58330d 100644 --- a/performance/benchmarks/TemplateCacheBench.php +++ b/performance/benchmarks/TemplateCacheBench.php @@ -2,125 +2,160 @@ namespace Keepsuit\Liquid\Performance\benchmarks; +use Keepsuit\Liquid\Contracts\LiquidTemplatesCache; use Keepsuit\Liquid\Environment; -use Keepsuit\Liquid\EnvironmentFactory; -use Keepsuit\Liquid\FileSystems\LocalFileSystem; -use Keepsuit\Liquid\Performance\Shopify\CommentFormTag; -use Keepsuit\Liquid\Performance\Shopify\CustomFilters; -use Keepsuit\Liquid\Performance\Shopify\Database; -use Keepsuit\Liquid\Performance\Shopify\PaginateTag; -use Keepsuit\Liquid\Support\Arr; +use Keepsuit\Liquid\Performance\benchmarks\Support\ComplexThemeFixture; use Keepsuit\Liquid\TemplatesCache\MemoryTemplatesCache; use Keepsuit\Liquid\TemplatesCache\SerializeTemplatesCache; use Keepsuit\Liquid\TemplatesCache\VarExportTemplatesCache; use PhpBench\Attributes\AfterMethods; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; use PhpBench\Attributes\OutputMode; use PhpBench\Attributes\OutputTimeUnit; use PhpBench\Attributes\Revs; -use PhpBench\Attributes\Warmup; -#[Iterations(10)] -#[Revs(10)] -#[Warmup(1)] +#[Groups(['macro'])] +#[Iterations(20)] +#[Revs(100)] #[OutputMode('throughput')] #[OutputTimeUnit('seconds', precision: 3)] #[AfterMethods('clearCache')] class TemplateCacheBench { - protected Environment $environment; + private const CACHE_DIRECTORY = 'keepsuit-liquid-phpbench'; - protected array $templates; + private Environment $environment; - #[BeforeMethods('setupInMemory')] - public function benchInMemory(): void + private LiquidTemplatesCache $cache; + + #[BeforeMethods('setUpInMemoryBuild')] + #[Revs(1000)] + public function benchBuildInMemory(): void { - $this->renderTemplates(); + $this->buildStaticTheme(); } - public function setupInMemory(): void + #[BeforeMethods('setUpInMemoryCachedRender')] + #[Revs(1000)] + public function benchLoadAndRenderInMemory(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new MemoryTemplatesCache) - ->build(); + $this->renderCachedTheme(); + } - $this->loadTemplates(); + #[BeforeMethods('setUpSerializeBuild')] + #[Revs(500)] + public function benchBuildSerialize(): void + { + $this->buildStaticTheme(); } - #[BeforeMethods('setupVarExporter')] - public function benchVarExporter(): void + #[BeforeMethods('setUpSerializeCachedRender')] + #[Revs(1000)] + public function benchLoadAndRenderSerialize(): void { - $this->renderTemplates(); + $this->renderCachedTheme(); } - public function setupVarExporter(): void + #[BeforeMethods('setUpVarExporterBuild')] + #[Revs(1000)] + public function benchBuildVarExporter(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new VarExportTemplatesCache(__DIR__.'/cache/var_export', keepInMemory: false)) - ->build(); + $this->buildStaticTheme(); + } - $this->loadTemplates(); + #[BeforeMethods('setUpVarExporterCachedRender')] + #[Revs(1000)] + public function benchLoadAndRenderVarExporter(): void + { + $this->renderCachedTheme(); } - #[BeforeMethods('setupSerialize')] - public function benchSerialize(): void + public function setUpInMemoryBuild(): void { - $this->renderTemplates(); + $this->setUpBuild('memory'); } - public function setupSerialize(): void + public function setUpInMemoryCachedRender(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new SerializeTemplatesCache(__DIR__.'/cache/serialize', keepInMemory: false)) - ->build(); + $this->setUpCachedRender('memory'); + } - $this->loadTemplates(); + public function setUpSerializeBuild(): void + { + $this->setUpBuild('serialize'); } - protected function loadTemplates(): void + public function setUpSerializeCachedRender(): void { - $baseDir = __DIR__.'/../tests'; - $files = glob($baseDir.'/**/*.liquid'); + $this->setUpCachedRender('serialize'); + } - if ($files === false) { - throw new \RuntimeException('Could not find any tests'); - } + public function setUpVarExporterBuild(): void + { + $this->setUpBuild('var-exporter'); + } - $this->templates = Arr::map($files, function (string $path) use ($baseDir) { - // relative path to the base directory - $name = str_replace($baseDir.'/', '', $path); - // remove .liquid extension - $name = substr($name, 0, -7); + public function setUpVarExporterCachedRender(): void + { + $this->setUpCachedRender('var-exporter'); + } - // replace / with . - return str_replace('/', '.', $name); - }); + public function clearCache(): void + { + $this->cache->clear(); + } - foreach ($this->templates as $template) { - $this->environment->parseTemplate($template); - } + private function setUpBuild(string $backend): void + { + $this->cache = $this->newCache($backend); + $this->cache->clear(); + $this->environment = ComplexThemeFixture::environment($this->cache); } - protected function environmentBuilder(): EnvironmentFactory + private function setUpCachedRender(string $backend): void { - return EnvironmentFactory::new() - ->setFilesystem(new LocalFileSystem(__DIR__.'/../tests')) - ->registerTag(CommentFormTag::class) - ->registerTag(PaginateTag::class) - ->registerFilters(CustomFilters::class); + $this->setUpBuild($backend); + $this->compileStaticTheme($this->environment); + + $this->cache = $backend === 'memory' + ? $this->cache + : $this->newCache($backend); + $this->environment = ComplexThemeFixture::environment($this->cache); } - protected function renderTemplates(): void + private function compileStaticTheme(Environment $environment): void { - foreach ($this->templates as $template) { - $this->environment->parseTemplate($template) - ->render($this->environment->newRenderContext(staticData: [...Database::tables()])); + foreach (array_keys(ComplexThemeFixture::templateSources()) as $templateName) { + $environment->parseTemplate($templateName); } } - public function clearCache(): void + private function buildStaticTheme(): void + { + $this->cache->clear(); + $this->compileStaticTheme($this->environment); + } + + private function renderCachedTheme(): void + { + $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()) + ->render(ComplexThemeFixture::newRenderContext($this->environment)); + } + + private function newCache(string $backend): LiquidTemplatesCache + { + return match ($backend) { + 'memory' => new MemoryTemplatesCache, + 'serialize' => new SerializeTemplatesCache($this->cachePath('serialize'), keepInMemory: false), + 'var-exporter' => new VarExportTemplatesCache($this->cachePath('var-exporter'), keepInMemory: false), + default => throw new \InvalidArgumentException("Unknown templates cache backend [$backend]."), + }; + } + + private function cachePath(string $backend): string { - $this->environment->templatesCache->clear(); + return sys_get_temp_dir().'/'.self::CACHE_DIRECTORY.'/'.$backend; } } diff --git a/performance/benchmarks/ThemeBench.php b/performance/benchmarks/ThemeBench.php new file mode 100644 index 0000000..e6811d1 --- /dev/null +++ b/performance/benchmarks/ThemeBench.php @@ -0,0 +1,63 @@ +environment = ComplexThemeFixture::environment(); + + foreach (array_keys(ComplexThemeFixture::templateSources()) as $name) { + $this->environment->parseTemplate($name); + } + + $this->template = $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); + } + + public function benchTokenize(): void + { + foreach (ComplexThemeFixture::templateSources() as $source) { + $this->environment->newParseContext()->tokenize($source); + } + } + + public function benchParse(): void + { + foreach (ComplexThemeFixture::templateSources() as $name => $source) { + $this->environment->parseString($source, $name); + } + } + + public function benchRender(): void + { + $this->template->render(ComplexThemeFixture::newRenderContext($this->environment)); + } + + #[Revs(1500)] + public function benchStream(): void + { + foreach ($this->template->stream(ComplexThemeFixture::newRenderContext($this->environment)) as $chunk) { + } + } +} diff --git a/performance/profile-theme.php b/performance/profile-theme.php new file mode 100644 index 0000000..c3125de --- /dev/null +++ b/performance/profile-theme.php @@ -0,0 +1,62 @@ + $arguments */ +$arguments = $_SERVER['argv'] ?? []; +for ($argumentIndex = 1; $argumentIndex < count($arguments); $argumentIndex++) { + $argument = $arguments[$argumentIndex]; + + if ($argument === '--help') { + fwrite(STDOUT, "Usage: php performance/profile-theme.php [--output=profile.json]\n"); + exit(0); + } + + if ($argument === '--output') { + $outputPath = $arguments[++$argumentIndex] ?? null; + } elseif (str_starts_with($argument, '--output=')) { + $outputPath = substr($argument, strlen('--output=')); + } else { + fwrite(STDERR, "Unknown argument: {$argument}\n"); + exit(1); + } + + if (! is_string($outputPath) || $outputPath === '') { + fwrite(STDERR, "The --output option requires a path.\n"); + exit(1); + } +} + +$environment = ComplexThemeFixture::environment(); +$environment->addExtension(new ProfilerExtension( + profiler: $profiler = new Profiler, + tags: true, + variables: true, +)); + +$template = $environment->parseString( + source: ComplexThemeFixture::rootTemplateSource(), + name: ComplexThemeFixture::rootTemplateName(), +); +$template->render(ComplexThemeFixture::newRenderContext($environment)); + +$report = ProfileReport::fromProfiler($profiler); +$json = json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; + +if ($outputPath === null) { + fwrite(STDOUT, $json); + exit(0); +} + +if (file_put_contents($outputPath, $json) === false) { + fwrite(STDERR, "Could not write profile report to {$outputPath}.\n"); + exit(1); +} + +fwrite(STDOUT, "Profile report written to {$outputPath}\n"); diff --git a/tests/Integration/Performance/ComplexThemeFixtureTest.php b/tests/Integration/Performance/ComplexThemeFixtureTest.php new file mode 100644 index 0000000..8829b2c --- /dev/null +++ b/tests/Integration/Performance/ComplexThemeFixtureTest.php @@ -0,0 +1,23 @@ +parseString( + ComplexThemeFixture::rootTemplateSource(), + ComplexThemeFixture::rootTemplateName(), + ); + + expect($template->render(ComplexThemeFixture::newRenderContext($environment)))->toBe(<<<'HTML' +
+

Summer Essentials

3 products

+
+

Linen Shirt

ACME APPAREL

€39.50

summer

Linen Shirt / ACME Apparel / 3950 | Linen Shirt / ACME Apparel / 3950

+

Canvas Tote

FIELD GOODS

€24.00

summer

Canvas Tote / Field Goods / 2400 | Canvas Tote / Field Goods / 2400

+

Sun Hat

COASTLINE

€18.75

summer

Sun Hat / Coastline / 1875 | Sun Hat / Coastline / 1875

+
+
+HTML); +}); diff --git a/tests/Unit/Performance/ProfileReportTest.php b/tests/Unit/Performance/ProfileReportTest.php new file mode 100644 index 0000000..13f90e9 --- /dev/null +++ b/tests/Unit/Performance/ProfileReportTest.php @@ -0,0 +1,43 @@ +enter(new Profile(ProfileType::Template, 'collection')); + $profiler->enter(new Profile(ProfileType::Variable, 'product.title')); + $profiler->leave(); + $profiler->leave(); + + $report = ProfileReport::fromProfiler($profiler); + /** @var array $rootProfile */ + $rootProfile = $report['profiles'][0]; + /** @var list> $children */ + $children = $rootProfile['children']; + + expect($report) + ->toHaveKeys(['schema_version', 'generated_at', 'php_version', 'profiles']) + ->and($report['schema_version'])->toBe(1) + ->and($report['profiles'])->toHaveCount(1) + ->and($rootProfile) + ->toMatchArray([ + 'type' => 'template', + 'name' => 'collection', + ]) + ->and($children) + ->toHaveCount(1) + ->and($children[0]) + ->toMatchArray([ + 'type' => 'variable', + 'name' => 'product.title', + 'children' => [], + ]) + ->and($rootProfile['duration'])->toBeGreaterThanOrEqual(0) + ->and($rootProfile['self_duration'])->toBeGreaterThanOrEqual(0) + ->and($rootProfile['memory_usage'])->toBeInt() + ->and($rootProfile['peak_memory_usage'])->toBeInt(); +}); diff --git a/tools/phpbench-compare.php b/tools/phpbench-compare.php index 9cd18d1..e9cc29f 100644 --- a/tools/phpbench-compare.php +++ b/tools/phpbench-compare.php @@ -17,8 +17,8 @@ sort($sharedNames); if ($sharedNames === []) { - fwrite(STDERR, "No common benchmark names found between base and PR results.\n"); - exit(2); + echo "> No comparable benchmark rows: the PR benchmark suite has changed. Establish a matching baseline on `main` before drawing performance conclusions.\n"; + exit(0); } // Runner jitter is routinely ±2%, so a single median over every benchmark is From 71581538229bfefe0a395c3b6204cb0363ef7d51 Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Thu, 30 Jul 2026 10:59:01 +0200 Subject: [PATCH 03/11] Use file-backed complex benchmark theme --- composer.json | 4 +- .../Support/ComplexThemeFixture.php | 109 ++++++++++++------ performance/benchmarks/TemplateCacheBench.php | 12 +- performance/benchmarks/ThemeBench.php | 15 ++- performance/profile-theme.php | 5 +- .../complex-collection/collection_grid.liquid | 3 + .../collection_header.liquid | 6 + .../collection_navigation.liquid | 5 + .../complex-collection/collection_page.liquid | 1 + .../complex-collection/page_shell.liquid | 5 + .../complex-collection/product_card.liquid | 9 ++ .../complex-collection/product_media.liquid | 4 + .../product_metadata.liquid | 8 ++ .../complex-collection/product_pricing.liquid | 5 + phpbench.json | 1 + .../Performance/ComplexThemeFixtureTest.php | 21 ++-- 16 files changed, 141 insertions(+), 72 deletions(-) create mode 100644 performance/themes/complex-collection/collection_grid.liquid create mode 100644 performance/themes/complex-collection/collection_header.liquid create mode 100644 performance/themes/complex-collection/collection_navigation.liquid create mode 100644 performance/themes/complex-collection/collection_page.liquid create mode 100644 performance/themes/complex-collection/page_shell.liquid create mode 100644 performance/themes/complex-collection/product_card.liquid create mode 100644 performance/themes/complex-collection/product_media.liquid create mode 100644 performance/themes/complex-collection/product_metadata.liquid create mode 100644 performance/themes/complex-collection/product_pricing.liquid diff --git a/composer.json b/composer.json index 2bb7f99..d9ac60f 100644 --- a/composer.json +++ b/composer.json @@ -51,8 +51,8 @@ "pint", "phpstan analyse" ], - "benchmark": "phpbench run --group=macro --report=aggregate", - "benchmark:micro": "phpbench run --group=micro --report=aggregate", + "benchmark": "phpbench run --group=macro --retry-threshold=5 --report=aggregate", + "benchmark:micro": "phpbench run --group=micro --retry-threshold=5 --report=aggregate", "profile": "phpbench xdebug:profile" }, "config": { diff --git a/performance/benchmarks/Support/ComplexThemeFixture.php b/performance/benchmarks/Support/ComplexThemeFixture.php index ade0a3b..180fd80 100644 --- a/performance/benchmarks/Support/ComplexThemeFixture.php +++ b/performance/benchmarks/Support/ComplexThemeFixture.php @@ -3,22 +3,49 @@ namespace Keepsuit\Liquid\Performance\benchmarks\Support; use Keepsuit\Liquid\Attributes\Cache; -use Keepsuit\Liquid\Contracts\LiquidFileSystem; use Keepsuit\Liquid\Contracts\LiquidTemplatesCache; use Keepsuit\Liquid\Drop; use Keepsuit\Liquid\Environment; use Keepsuit\Liquid\EnvironmentFactory; +use Keepsuit\Liquid\FileSystems\LocalFileSystem; use Keepsuit\Liquid\Filters\FiltersProvider; use Keepsuit\Liquid\Render\RenderContext; final class ComplexThemeFixture { - public const ROOT_TEMPLATE_NAME = 'collection-page'; + public const ROOT_TEMPLATE_NAME = 'collection_page'; + + private const PRODUCTS = [ + ['linen-shirt', 'Linen Shirt', 'ACME Apparel', 3950, 5900, ['new', 'linen']], + ['canvas-tote', 'Canvas Tote', 'Field Goods', 2400, 3200, ['travel', 'sale']], + ['sun-hat', 'Sun Hat', 'Coastline', 1875, 2500, ['summer', 'sale']], + ['ceramic-mug', 'Ceramic Mug', 'Studio Form', 2200, 2200, ['kitchen', 'new']], + ['weekend-bag', 'Weekend Bag', 'Field Goods', 8900, 10900, ['travel', 'limited']], + ['cotton-throw', 'Cotton Throw', 'North Loom', 7600, 7600, ['home', 'soft']], + ['leather-wallet', 'Leather Wallet', 'Atelier No. 8', 5200, 6500, ['gift', 'sale']], + ['travel-bottle', 'Travel Bottle', 'Coastline', 2800, 2800, ['travel', 'summer']], + ['desk-lamp', 'Desk Lamp', 'Studio Form', 6800, 8100, ['home', 'new']], + ['wool-socks', 'Wool Socks', 'North Loom', 1600, 1600, ['warm', 'gift']], + ['market-basket', 'Market Basket', 'Field Goods', 4300, 5000, ['home', 'limited']], + ['notebook-set', 'Notebook Set', 'Paper Mill', 1900, 1900, ['desk', 'new']], + ['silk-scarf', 'Silk Scarf', 'Atelier No. 8', 7400, 8900, ['gift', 'sale']], + ['beach-towel', 'Beach Towel', 'Coastline', 3600, 3600, ['summer', 'travel']], + ['glass-vase', 'Glass Vase', 'Studio Form', 4600, 5200, ['home', 'limited']], + ['knit-cap', 'Knit Cap', 'North Loom', 3100, 3100, ['warm', 'new']], + ['key-organizer', 'Key Organizer', 'Atelier No. 8', 4100, 4800, ['gift', 'travel']], + ['picnic-blanket', 'Picnic Blanket', 'Field Goods', 9200, 11200, ['summer', 'home']], + ['tea-canister', 'Tea Canister', 'Paper Mill', 2700, 2700, ['kitchen', 'desk']], + ['camp-lantern', 'Camp Lantern', 'Coastline', 5400, 6200, ['travel', 'limited']], + ['table-clock', 'Table Clock', 'Studio Form', 5700, 5700, ['home', 'desk']], + ['cashmere-wrap', 'Cashmere Wrap', 'North Loom', 12800, 14900, ['warm', 'gift']], + ['card-holder', 'Card Holder', 'Atelier No. 8', 3300, 3300, ['gift', 'new']], + ['sketchbook', 'Sketchbook', 'Paper Mill', 2500, 2500, ['desk', 'travel']], + ]; public static function environment(?LiquidTemplatesCache $templatesCache = null): Environment { $factory = EnvironmentFactory::new() - ->setFilesystem(new ComplexThemeFileSystem(self::templateSources())) + ->setFilesystem(new LocalFileSystem(self::themePath())) ->registerFilters(ComplexThemeFilters::class); if ($templatesCache !== null) { @@ -28,29 +55,45 @@ public static function environment(?LiquidTemplatesCache $templatesCache = null) return $factory->build(); } - public static function rootTemplateName(): string + public static function themePath(): string { - return self::ROOT_TEMPLATE_NAME; + return dirname(__DIR__, 2).'/themes/complex-collection'; } - public static function rootTemplateSource(): string + public static function rootTemplateName(): string { - return self::templateSources()[self::ROOT_TEMPLATE_NAME]; + return self::ROOT_TEMPLATE_NAME; } /** - * @return array + * @return list */ - public static function templateSources(): array + public static function templateNames(): array { return [ - self::ROOT_TEMPLATE_NAME => '{% render "page-shell", collection: collection %}', - 'page-shell' => "
\n {% render \"collection-section\", collection: collection %}\n
", - 'collection-section' => "

{{ collection.title }}

{{ collection.products | size }} products

\n
\n{% for product in collection.products %} {% render \"product-card\", product: product, label: collection.label %}\n{% endfor %}
", - 'product-card' => '

{{ product.title }}

{{ product.vendor | upcase }}

{{ product.price_cents | fixture_money }}

{{ label }}

{{ product.title }} / {{ product.vendor }} / {{ product.inventory_label }} | {{ product.title }} / {{ product.vendor }} / {{ product.inventory_label }}

', + 'collection_page', + 'page_shell', + 'collection_header', + 'collection_navigation', + 'collection_grid', + 'product_card', + 'product_media', + 'product_pricing', + 'product_metadata', ]; } + public static function templateSource(string $templateName): string + { + $source = file_get_contents(self::themePath().'/'.$templateName.'.liquid'); + + if ($source === false) { + throw new \RuntimeException("Could not read fixture template [$templateName]."); + } + + return $source; + } + /** * @return array */ @@ -61,11 +104,12 @@ public static function renderData(): array 'handle' => 'summer-essentials', 'title' => 'Summer Essentials', 'label' => 'summer', - 'products' => [ - new ComplexThemeProduct('linen-shirt', 'Linen Shirt', 'ACME Apparel', 3950), - new ComplexThemeProduct('canvas-tote', 'Canvas Tote', 'Field Goods', 2400), - new ComplexThemeProduct('sun-hat', 'Sun Hat', 'Coastline', 1875), - ], + 'description' => 'Everyday pieces for long weekends, slow mornings, and bright afternoons.', + 'tags' => ['New arrivals', 'Travel ready', 'Summer layers', 'Gifts under €100'], + 'products' => array_map( + static fn (array $product) => new ComplexThemeProduct(...$product), + self::PRODUCTS, + ), ], ]; } @@ -76,21 +120,6 @@ public static function newRenderContext(Environment $environment): RenderContext } } -final class ComplexThemeFileSystem implements LiquidFileSystem -{ - /** - * @param array $templates - */ - public function __construct( - private readonly array $templates, - ) {} - - public function readTemplateFile(string $templateName): string - { - return $this->templates[$templateName] ?? throw new \RuntimeException("Unknown fixture template [$templateName]."); - } -} - final class ComplexThemeFilters extends FiltersProvider { public function fixtureMoney(int|float $cents): string @@ -101,11 +130,16 @@ public function fixtureMoney(int|float $cents): string final class ComplexThemeProduct extends Drop { + /** + * @param list $badges + */ public function __construct( public readonly string $handle, public readonly string $title, private readonly string $vendorName, private readonly int $priceCents, + private readonly int $compareAtPriceCents, + public readonly array $badges, ) {} public function vendor(): string @@ -118,9 +152,14 @@ public function priceCents(): int return $this->priceCents; } + public function compareAtPriceCents(): int + { + return $this->compareAtPriceCents; + } + #[Cache] - public function inventoryLabel(): int + public function inventoryLabel(): string { - return $this->priceCents; + return $this->priceCents < 3000 ? 'Low stock' : 'In stock'; } } diff --git a/performance/benchmarks/TemplateCacheBench.php b/performance/benchmarks/TemplateCacheBench.php index a58330d..48f6dfe 100644 --- a/performance/benchmarks/TemplateCacheBench.php +++ b/performance/benchmarks/TemplateCacheBench.php @@ -17,8 +17,8 @@ use PhpBench\Attributes\Revs; #[Groups(['macro'])] -#[Iterations(20)] -#[Revs(100)] +#[Iterations(10)] +#[Revs(20)] #[OutputMode('throughput')] #[OutputTimeUnit('seconds', precision: 3)] #[AfterMethods('clearCache')] @@ -31,42 +31,36 @@ class TemplateCacheBench private LiquidTemplatesCache $cache; #[BeforeMethods('setUpInMemoryBuild')] - #[Revs(1000)] public function benchBuildInMemory(): void { $this->buildStaticTheme(); } #[BeforeMethods('setUpInMemoryCachedRender')] - #[Revs(1000)] public function benchLoadAndRenderInMemory(): void { $this->renderCachedTheme(); } #[BeforeMethods('setUpSerializeBuild')] - #[Revs(500)] public function benchBuildSerialize(): void { $this->buildStaticTheme(); } #[BeforeMethods('setUpSerializeCachedRender')] - #[Revs(1000)] public function benchLoadAndRenderSerialize(): void { $this->renderCachedTheme(); } #[BeforeMethods('setUpVarExporterBuild')] - #[Revs(1000)] public function benchBuildVarExporter(): void { $this->buildStaticTheme(); } #[BeforeMethods('setUpVarExporterCachedRender')] - #[Revs(1000)] public function benchLoadAndRenderVarExporter(): void { $this->renderCachedTheme(); @@ -127,7 +121,7 @@ private function setUpCachedRender(string $backend): void private function compileStaticTheme(Environment $environment): void { - foreach (array_keys(ComplexThemeFixture::templateSources()) as $templateName) { + foreach (ComplexThemeFixture::templateNames() as $templateName) { $environment->parseTemplate($templateName); } } diff --git a/performance/benchmarks/ThemeBench.php b/performance/benchmarks/ThemeBench.php index e6811d1..1ded8cb 100644 --- a/performance/benchmarks/ThemeBench.php +++ b/performance/benchmarks/ThemeBench.php @@ -13,8 +13,8 @@ use PhpBench\Attributes\Revs; #[Groups(['macro'])] -#[Iterations(20)] -#[Revs(1000)] +#[Iterations(10)] +#[Revs(20)] #[OutputMode('throughput')] #[OutputTimeUnit('seconds', precision: 3)] #[BeforeMethods('setUp')] @@ -28,7 +28,7 @@ public function setUp(): void { $this->environment = ComplexThemeFixture::environment(); - foreach (array_keys(ComplexThemeFixture::templateSources()) as $name) { + foreach (ComplexThemeFixture::templateNames() as $name) { $this->environment->parseTemplate($name); } @@ -37,15 +37,15 @@ public function setUp(): void public function benchTokenize(): void { - foreach (ComplexThemeFixture::templateSources() as $source) { - $this->environment->newParseContext()->tokenize($source); + foreach (ComplexThemeFixture::templateNames() as $name) { + $this->environment->newParseContext()->tokenize(ComplexThemeFixture::templateSource($name)); } } public function benchParse(): void { - foreach (ComplexThemeFixture::templateSources() as $name => $source) { - $this->environment->parseString($source, $name); + foreach (ComplexThemeFixture::templateNames() as $name) { + $this->environment->parseString(ComplexThemeFixture::templateSource($name), $name); } } @@ -54,7 +54,6 @@ public function benchRender(): void $this->template->render(ComplexThemeFixture::newRenderContext($this->environment)); } - #[Revs(1500)] public function benchStream(): void { foreach ($this->template->stream(ComplexThemeFixture::newRenderContext($this->environment)) as $chunk) { diff --git a/performance/profile-theme.php b/performance/profile-theme.php index c3125de..bb8a2c8 100644 --- a/performance/profile-theme.php +++ b/performance/profile-theme.php @@ -40,10 +40,7 @@ variables: true, )); -$template = $environment->parseString( - source: ComplexThemeFixture::rootTemplateSource(), - name: ComplexThemeFixture::rootTemplateName(), -); +$template = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); $template->render(ComplexThemeFixture::newRenderContext($environment)); $report = ProfileReport::fromProfiler($profiler); diff --git a/performance/themes/complex-collection/collection_grid.liquid b/performance/themes/complex-collection/collection_grid.liquid new file mode 100644 index 0000000..eef4ce2 --- /dev/null +++ b/performance/themes/complex-collection/collection_grid.liquid @@ -0,0 +1,3 @@ +
+{% for product in collection.products %} {% render "product_card", product: product, label: collection.label, position: forloop.index %} +{% endfor %}
diff --git a/performance/themes/complex-collection/collection_header.liquid b/performance/themes/complex-collection/collection_header.liquid new file mode 100644 index 0000000..e521680 --- /dev/null +++ b/performance/themes/complex-collection/collection_header.liquid @@ -0,0 +1,6 @@ +
+

{{ collection.label | upcase | append: " collection" }}

+

{{ collection.title }}

+

{{ collection.description }}

+

{{ collection.products | size }} products selected

+
diff --git a/performance/themes/complex-collection/collection_navigation.liquid b/performance/themes/complex-collection/collection_navigation.liquid new file mode 100644 index 0000000..4d7b3e5 --- /dev/null +++ b/performance/themes/complex-collection/collection_navigation.liquid @@ -0,0 +1,5 @@ + diff --git a/performance/themes/complex-collection/collection_page.liquid b/performance/themes/complex-collection/collection_page.liquid new file mode 100644 index 0000000..7d509e2 --- /dev/null +++ b/performance/themes/complex-collection/collection_page.liquid @@ -0,0 +1 @@ +{% render "page_shell", collection: collection %} diff --git a/performance/themes/complex-collection/page_shell.liquid b/performance/themes/complex-collection/page_shell.liquid new file mode 100644 index 0000000..e0b817b --- /dev/null +++ b/performance/themes/complex-collection/page_shell.liquid @@ -0,0 +1,5 @@ +
+ {% render "collection_header", collection: collection %} + {% render "collection_navigation", collection: collection %} + {% render "collection_grid", collection: collection %} +
diff --git a/performance/themes/complex-collection/product_card.liquid b/performance/themes/complex-collection/product_card.liquid new file mode 100644 index 0000000..9a56c60 --- /dev/null +++ b/performance/themes/complex-collection/product_card.liquid @@ -0,0 +1,9 @@ +
+ {% render "product_media", product: product %} +
+

{{ product.vendor | upcase }}

+

{{ product.title }}

+ {% render "product_pricing", product: product %} + {% render "product_metadata", product: product, label: label %} +
+
diff --git a/performance/themes/complex-collection/product_media.liquid b/performance/themes/complex-collection/product_media.liquid new file mode 100644 index 0000000..3a073a6 --- /dev/null +++ b/performance/themes/complex-collection/product_media.liquid @@ -0,0 +1,4 @@ +
+ {{ product.title | escape }} +
{{ product.title }} by {{ product.vendor }}
+
diff --git a/performance/themes/complex-collection/product_metadata.liquid b/performance/themes/complex-collection/product_metadata.liquid new file mode 100644 index 0000000..c7d125d --- /dev/null +++ b/performance/themes/complex-collection/product_metadata.liquid @@ -0,0 +1,8 @@ + diff --git a/performance/themes/complex-collection/product_pricing.liquid b/performance/themes/complex-collection/product_pricing.liquid new file mode 100644 index 0000000..47219fe --- /dev/null +++ b/performance/themes/complex-collection/product_pricing.liquid @@ -0,0 +1,5 @@ +

+ {{ product.price_cents | fixture_money }} + {% if product.compare_at_price_cents > product.price_cents %}{{ product.compare_at_price_cents | fixture_money }}{% endif %} + {{ product.compare_at_price_cents | minus: product.price_cents | fixture_money }} +

diff --git a/phpbench.json b/phpbench.json index 753df5e..78c320c 100644 --- a/phpbench.json +++ b/phpbench.json @@ -2,6 +2,7 @@ "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", "runner.path": "performance/benchmarks", + "runner.retry_threshold": 5, "runner.php_config": { "opcache.enable": 1, "opcache.enable_cli": 1, diff --git a/tests/Integration/Performance/ComplexThemeFixtureTest.php b/tests/Integration/Performance/ComplexThemeFixtureTest.php index 8829b2c..83d542c 100644 --- a/tests/Integration/Performance/ComplexThemeFixtureTest.php +++ b/tests/Integration/Performance/ComplexThemeFixtureTest.php @@ -5,19 +5,12 @@ test('complex theme fixture renders the deterministic collection page', function () { $environment = ComplexThemeFixture::environment(); - $template = $environment->parseString( - ComplexThemeFixture::rootTemplateSource(), - ComplexThemeFixture::rootTemplateName(), - ); + $template = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); - expect($template->render(ComplexThemeFixture::newRenderContext($environment)))->toBe(<<<'HTML' -
-

Summer Essentials

3 products

-
-

Linen Shirt

ACME APPAREL

€39.50

summer

Linen Shirt / ACME Apparel / 3950 | Linen Shirt / ACME Apparel / 3950

-

Canvas Tote

FIELD GOODS

€24.00

summer

Canvas Tote / Field Goods / 2400 | Canvas Tote / Field Goods / 2400

-

Sun Hat

COASTLINE

€18.75

summer

Sun Hat / Coastline / 1875 | Sun Hat / Coastline / 1875

-
-
-HTML); + $rendered = $template->render(ComplexThemeFixture::newRenderContext($environment)); + + expect($rendered) + ->toContain('24 products selected') + ->toContain('data-handle="sketchbook"') + ->and(hash('sha256', $rendered))->toBe('17ee8672cb258106bb58a0642a4fa55cabc66163f0b27cdb37c9f3392fb80a45'); }); From b43b5202a1bf050a53c96992e89167e9e6d90186 Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Thu, 30 Jul 2026 11:15:45 +0200 Subject: [PATCH 04/11] Use Shopify-style benchmark theme layout --- .../Support/ComplexThemeFixture.php | 42 +++++++++++++++++-- performance/benchmarks/ThemeBench.php | 15 +++++-- performance/profile-theme.php | 6 ++- .../{page_shell.liquid => collection.liquid} | 5 +++ .../complex-collection/collection_page.liquid | 1 - .../themes/complex-collection/theme.liquid | 33 +++++++++++++++ .../Performance/ComplexThemeFixtureTest.php | 16 +++++-- 7 files changed, 105 insertions(+), 13 deletions(-) rename performance/themes/complex-collection/{page_shell.liquid => collection.liquid} (58%) delete mode 100644 performance/themes/complex-collection/collection_page.liquid create mode 100644 performance/themes/complex-collection/theme.liquid diff --git a/performance/benchmarks/Support/ComplexThemeFixture.php b/performance/benchmarks/Support/ComplexThemeFixture.php index 180fd80..c8b0624 100644 --- a/performance/benchmarks/Support/ComplexThemeFixture.php +++ b/performance/benchmarks/Support/ComplexThemeFixture.php @@ -13,7 +13,9 @@ final class ComplexThemeFixture { - public const ROOT_TEMPLATE_NAME = 'collection_page'; + public const ROOT_TEMPLATE_NAME = 'collection'; + + public const LAYOUT_TEMPLATE_NAME = 'theme'; private const PRODUCTS = [ ['linen-shirt', 'Linen Shirt', 'ACME Apparel', 3950, 5900, ['new', 'linen']], @@ -71,8 +73,8 @@ public static function rootTemplateName(): string public static function templateNames(): array { return [ - 'collection_page', - 'page_shell', + self::ROOT_TEMPLATE_NAME, + self::LAYOUT_TEMPLATE_NAME, 'collection_header', 'collection_navigation', 'collection_grid', @@ -100,6 +102,32 @@ public static function templateSource(string $templateName): string public static function renderData(): array { return [ + 'shop' => [ + 'name' => 'Northstar Goods', + 'currency' => 'EUR', + ], + 'cart' => [ + 'item_count' => 3, + 'total_price' => 12650, + ], + 'linklists' => [ + 'main-menu' => [ + 'links' => [ + ['url' => '/collections/summer-essentials', 'title' => 'Summer'], + ['url' => '/collections/travel', 'title' => 'Travel'], + ['url' => '/collections/home', 'title' => 'Home'], + ], + ], + 'footer' => [ + 'links' => [ + ['url' => '/pages/shipping', 'title' => 'Shipping'], + ['url' => '/pages/returns', 'title' => 'Returns'], + ['url' => '/pages/contact', 'title' => 'Contact'], + ], + ], + ], + 'page_title' => 'Summer Essentials', + 'template' => 'collection', 'collection' => [ 'handle' => 'summer-essentials', 'title' => 'Summer Essentials', @@ -118,6 +146,14 @@ public static function newRenderContext(Environment $environment): RenderContext { return $environment->newRenderContext(staticData: self::renderData()); } + + public static function newLayoutRenderContext(Environment $environment, mixed $content): RenderContext + { + return $environment->newRenderContext(staticData: [ + ...self::renderData(), + 'content_for_layout' => $content, + ]); + } } final class ComplexThemeFilters extends FiltersProvider diff --git a/performance/benchmarks/ThemeBench.php b/performance/benchmarks/ThemeBench.php index 1ded8cb..4802ab6 100644 --- a/performance/benchmarks/ThemeBench.php +++ b/performance/benchmarks/ThemeBench.php @@ -22,7 +22,9 @@ class ThemeBench { private Environment $environment; - private Template $template; + private Template $page; + + private Template $layout; public function setUp(): void { @@ -32,7 +34,8 @@ public function setUp(): void $this->environment->parseTemplate($name); } - $this->template = $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); + $this->page = $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); + $this->layout = $this->environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); } public function benchTokenize(): void @@ -51,12 +54,16 @@ public function benchParse(): void public function benchRender(): void { - $this->template->render(ComplexThemeFixture::newRenderContext($this->environment)); + $content = $this->page->render(ComplexThemeFixture::newRenderContext($this->environment)); + + $this->layout->render(ComplexThemeFixture::newLayoutRenderContext($this->environment, $content)); } public function benchStream(): void { - foreach ($this->template->stream(ComplexThemeFixture::newRenderContext($this->environment)) as $chunk) { + $content = $this->page->stream(ComplexThemeFixture::newRenderContext($this->environment)); + + foreach ($this->layout->stream(ComplexThemeFixture::newLayoutRenderContext($this->environment, $content)) as $chunk) { } } } diff --git a/performance/profile-theme.php b/performance/profile-theme.php index bb8a2c8..c14eef3 100644 --- a/performance/profile-theme.php +++ b/performance/profile-theme.php @@ -40,8 +40,10 @@ variables: true, )); -$template = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); -$template->render(ComplexThemeFixture::newRenderContext($environment)); +$page = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); +$layout = $environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); +$content = $page->render(ComplexThemeFixture::newRenderContext($environment)); +$layout->render(ComplexThemeFixture::newLayoutRenderContext($environment, $content)); $report = ProfileReport::fromProfiler($profiler); $json = json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; diff --git a/performance/themes/complex-collection/page_shell.liquid b/performance/themes/complex-collection/collection.liquid similarity index 58% rename from performance/themes/complex-collection/page_shell.liquid rename to performance/themes/complex-collection/collection.liquid index e0b817b..fc67740 100644 --- a/performance/themes/complex-collection/page_shell.liquid +++ b/performance/themes/complex-collection/collection.liquid @@ -1,5 +1,10 @@ +{% assign product_count = collection.products | size %} +{% if product_count > 0 %}
{% render "collection_header", collection: collection %} {% render "collection_navigation", collection: collection %} {% render "collection_grid", collection: collection %}
+{% else %} +

No products found in this collection.

+{% endif %} diff --git a/performance/themes/complex-collection/collection_page.liquid b/performance/themes/complex-collection/collection_page.liquid deleted file mode 100644 index 7d509e2..0000000 --- a/performance/themes/complex-collection/collection_page.liquid +++ /dev/null @@ -1 +0,0 @@ -{% render "page_shell", collection: collection %} diff --git a/performance/themes/complex-collection/theme.liquid b/performance/themes/complex-collection/theme.liquid new file mode 100644 index 0000000..a988084 --- /dev/null +++ b/performance/themes/complex-collection/theme.liquid @@ -0,0 +1,33 @@ + + + + {{ shop.name }} — {{ page_title }} + + + + +
+ + {{ content_for_layout }} +
+
+

All prices are in {{ shop.currency }}.

+ +
+ + diff --git a/tests/Integration/Performance/ComplexThemeFixtureTest.php b/tests/Integration/Performance/ComplexThemeFixtureTest.php index 83d542c..4135141 100644 --- a/tests/Integration/Performance/ComplexThemeFixtureTest.php +++ b/tests/Integration/Performance/ComplexThemeFixtureTest.php @@ -5,12 +5,22 @@ test('complex theme fixture renders the deterministic collection page', function () { $environment = ComplexThemeFixture::environment(); - $template = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); + $page = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); + $layout = $environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); - $rendered = $template->render(ComplexThemeFixture::newRenderContext($environment)); + $content = $page->render(ComplexThemeFixture::newRenderContext($environment)); + $rendered = $layout->render(ComplexThemeFixture::newLayoutRenderContext($environment, $content)); + + $streamedContent = $page->stream(ComplexThemeFixture::newRenderContext($environment)); + $streamed = implode('', iterator_to_array( + $layout->stream(ComplexThemeFixture::newLayoutRenderContext($environment, $streamedContent)), + false, + )); expect($rendered) + ->toContain('Northstar Goods — Summer Essentials') ->toContain('24 products selected') ->toContain('data-handle="sketchbook"') - ->and(hash('sha256', $rendered))->toBe('17ee8672cb258106bb58a0642a4fa55cabc66163f0b27cdb37c9f3392fb80a45'); + ->and(hash('sha256', $rendered))->toBe('52a906d56a26957edbf34fc85ed8dc76e3ccb74f9aa012effd5fd84de5e732eb') + ->and($streamed)->toBe($rendered); }); From 395d84898561c4e6a0eeb480cae9cab0e0cbf087 Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Thu, 30 Jul 2026 11:30:00 +0200 Subject: [PATCH 05/11] Structure benchmark theme into pages and snippets --- .../Support/ComplexThemeFixture.php | 124 ++++++++++++++---- performance/benchmarks/TemplateCacheBench.php | 5 +- performance/benchmarks/ThemeBench.php | 20 +-- performance/profile-theme.php | 7 +- .../complex-collection/collection.liquid | 10 -- .../complex-collection/collection_grid.liquid | 3 - .../collection_navigation.liquid | 5 - .../complex-collection/layout/theme.liquid | 12 ++ .../snippets/collection/grid.liquid | 3 + .../collection/header.liquid} | 1 + .../snippets/collection/navigation.liquid | 6 + .../snippets/index/featured_products.liquid | 6 + .../snippets/index/hero.liquid | 6 + .../snippets/index/journal.liquid | 6 + .../snippets/page/content.liquid | 4 + .../snippets/page/header.liquid | 4 + .../product/card.liquid} | 6 +- .../snippets/product/detail.liquid | 10 ++ .../product/media.liquid} | 0 .../product/metadata.liquid} | 0 .../product/pricing.liquid} | 4 +- .../snippets/shared/button.liquid | 1 + .../snippets/shared/icon.liquid | 3 + .../snippets/shared/price.liquid | 1 + .../snippets/shared/site_footer.liquid | 6 + .../snippets/shared/site_header.liquid | 9 ++ .../templates/collection.liquid | 10 ++ .../complex-collection/templates/index.liquid | 5 + .../complex-collection/templates/page.liquid | 4 + .../templates/product.liquid | 3 + .../themes/complex-collection/theme.liquid | 33 ----- .../Performance/ComplexThemeFixtureTest.php | 43 +++--- 32 files changed, 243 insertions(+), 117 deletions(-) delete mode 100644 performance/themes/complex-collection/collection.liquid delete mode 100644 performance/themes/complex-collection/collection_grid.liquid delete mode 100644 performance/themes/complex-collection/collection_navigation.liquid create mode 100644 performance/themes/complex-collection/layout/theme.liquid create mode 100644 performance/themes/complex-collection/snippets/collection/grid.liquid rename performance/themes/complex-collection/{collection_header.liquid => snippets/collection/header.liquid} (66%) create mode 100644 performance/themes/complex-collection/snippets/collection/navigation.liquid create mode 100644 performance/themes/complex-collection/snippets/index/featured_products.liquid create mode 100644 performance/themes/complex-collection/snippets/index/hero.liquid create mode 100644 performance/themes/complex-collection/snippets/index/journal.liquid create mode 100644 performance/themes/complex-collection/snippets/page/content.liquid create mode 100644 performance/themes/complex-collection/snippets/page/header.liquid rename performance/themes/complex-collection/{product_card.liquid => snippets/product/card.liquid} (56%) create mode 100644 performance/themes/complex-collection/snippets/product/detail.liquid rename performance/themes/complex-collection/{product_media.liquid => snippets/product/media.liquid} (100%) rename performance/themes/complex-collection/{product_metadata.liquid => snippets/product/metadata.liquid} (100%) rename performance/themes/complex-collection/{product_pricing.liquid => snippets/product/pricing.liquid} (57%) create mode 100644 performance/themes/complex-collection/snippets/shared/button.liquid create mode 100644 performance/themes/complex-collection/snippets/shared/icon.liquid create mode 100644 performance/themes/complex-collection/snippets/shared/price.liquid create mode 100644 performance/themes/complex-collection/snippets/shared/site_footer.liquid create mode 100644 performance/themes/complex-collection/snippets/shared/site_header.liquid create mode 100644 performance/themes/complex-collection/templates/collection.liquid create mode 100644 performance/themes/complex-collection/templates/index.liquid create mode 100644 performance/themes/complex-collection/templates/page.liquid create mode 100644 performance/themes/complex-collection/templates/product.liquid delete mode 100644 performance/themes/complex-collection/theme.liquid diff --git a/performance/benchmarks/Support/ComplexThemeFixture.php b/performance/benchmarks/Support/ComplexThemeFixture.php index c8b0624..0dcceb0 100644 --- a/performance/benchmarks/Support/ComplexThemeFixture.php +++ b/performance/benchmarks/Support/ComplexThemeFixture.php @@ -13,9 +13,16 @@ final class ComplexThemeFixture { - public const ROOT_TEMPLATE_NAME = 'collection'; + public const ROOT_TEMPLATE_NAME = 'templates.collection'; - public const LAYOUT_TEMPLATE_NAME = 'theme'; + public const LAYOUT_TEMPLATE_NAME = 'layout.theme'; + + private const PAGE_TEMPLATE_NAMES = [ + 'templates.index', + self::ROOT_TEMPLATE_NAME, + 'templates.product', + 'templates.page', + ]; private const PRODUCTS = [ ['linen-shirt', 'Linen Shirt', 'ACME Apparel', 3950, 5900, ['new', 'linen']], @@ -73,21 +80,40 @@ public static function rootTemplateName(): string public static function templateNames(): array { return [ - self::ROOT_TEMPLATE_NAME, self::LAYOUT_TEMPLATE_NAME, - 'collection_header', - 'collection_navigation', - 'collection_grid', - 'product_card', - 'product_media', - 'product_pricing', - 'product_metadata', + ...self::PAGE_TEMPLATE_NAMES, + 'snippets.shared.site_header', + 'snippets.shared.site_footer', + 'snippets.shared.button', + 'snippets.shared.icon', + 'snippets.shared.price', + 'snippets.collection.header', + 'snippets.collection.navigation', + 'snippets.collection.grid', + 'snippets.index.hero', + 'snippets.index.featured_products', + 'snippets.index.journal', + 'snippets.product.card', + 'snippets.product.media', + 'snippets.product.pricing', + 'snippets.product.metadata', + 'snippets.product.detail', + 'snippets.page.header', + 'snippets.page.content', ]; } + /** + * @return list + */ + public static function pageTemplateNames(): array + { + return self::PAGE_TEMPLATE_NAMES; + } + public static function templateSource(string $templateName): string { - $source = file_get_contents(self::themePath().'/'.$templateName.'.liquid'); + $source = file_get_contents(self::templatePath($templateName)); if ($source === false) { throw new \RuntimeException("Could not read fixture template [$templateName]."); @@ -96,11 +122,23 @@ public static function templateSource(string $templateName): string return $source; } + public static function templatePath(string $templateName): string + { + return self::themePath().'/'.str_replace('.', '/', $templateName).'.liquid'; + } + /** * @return array */ - public static function renderData(): array + public static function renderData(string $pageTemplateName = self::ROOT_TEMPLATE_NAME): array { + $products = array_map( + static fn (array $product) => new ComplexThemeProduct(...$product), + self::PRODUCTS, + ); + + $pageType = str_replace('templates.', '', $pageTemplateName); + return [ 'shop' => [ 'name' => 'Northstar Goods', @@ -126,34 +164,72 @@ public static function renderData(): array ], ], ], - 'page_title' => 'Summer Essentials', - 'template' => 'collection', + 'page_title' => match ($pageType) { + 'index' => 'New arrivals', + 'product' => 'Weekend Bag', + 'page' => 'Our story', + default => 'Summer Essentials', + }, + 'template' => $pageType, 'collection' => [ 'handle' => 'summer-essentials', 'title' => 'Summer Essentials', 'label' => 'summer', 'description' => 'Everyday pieces for long weekends, slow mornings, and bright afternoons.', 'tags' => ['New arrivals', 'Travel ready', 'Summer layers', 'Gifts under €100'], - 'products' => array_map( - static fn (array $product) => new ComplexThemeProduct(...$product), - self::PRODUCTS, - ), + 'products' => $products, + ], + 'product' => $products[4], + 'page' => [ + 'title' => 'Our story', + 'content' => 'Northstar Goods makes useful objects for slower days, closer places, and longer weekends.', + ], + 'articles' => [ + ['title' => 'Packing light for a weekend away', 'excerpt' => 'Three small choices that make travel easier.'], + ['title' => 'How to choose a daily carry', 'excerpt' => 'A practical guide to materials and proportions.'], + ['title' => 'The objects that earn their place', 'excerpt' => 'On buying less and using more.'], ], ]; } - public static function newRenderContext(Environment $environment): RenderContext - { - return $environment->newRenderContext(staticData: self::renderData()); + public static function newRenderContext( + Environment $environment, + string $pageTemplateName = self::ROOT_TEMPLATE_NAME, + ): RenderContext { + return $environment->newRenderContext(staticData: self::renderData($pageTemplateName)); } - public static function newLayoutRenderContext(Environment $environment, mixed $content): RenderContext - { + public static function newLayoutRenderContext( + Environment $environment, + mixed $content, + string $pageTemplateName = self::ROOT_TEMPLATE_NAME, + ): RenderContext { return $environment->newRenderContext(staticData: [ - ...self::renderData(), + ...self::renderData($pageTemplateName), 'content_for_layout' => $content, ]); } + + public static function renderPage(Environment $environment, string $pageTemplateName): string + { + $content = $environment->parseTemplate($pageTemplateName) + ->render(self::newRenderContext($environment, $pageTemplateName)); + + return $environment->parseTemplate(self::LAYOUT_TEMPLATE_NAME) + ->render(self::newLayoutRenderContext($environment, $content, $pageTemplateName)); + } + + /** + * @return \Generator + */ + public static function streamPage(Environment $environment, string $pageTemplateName): \Generator + { + $content = $environment->parseTemplate($pageTemplateName) + ->stream(self::newRenderContext($environment, $pageTemplateName)); + + return $environment->parseTemplate(self::LAYOUT_TEMPLATE_NAME) + ->stream(self::newLayoutRenderContext($environment, $content, $pageTemplateName)); + } } final class ComplexThemeFilters extends FiltersProvider diff --git a/performance/benchmarks/TemplateCacheBench.php b/performance/benchmarks/TemplateCacheBench.php index 48f6dfe..5c0c479 100644 --- a/performance/benchmarks/TemplateCacheBench.php +++ b/performance/benchmarks/TemplateCacheBench.php @@ -134,8 +134,9 @@ private function buildStaticTheme(): void private function renderCachedTheme(): void { - $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()) - ->render(ComplexThemeFixture::newRenderContext($this->environment)); + foreach (ComplexThemeFixture::pageTemplateNames() as $templateName) { + ComplexThemeFixture::renderPage($this->environment, $templateName); + } } private function newCache(string $backend): LiquidTemplatesCache diff --git a/performance/benchmarks/ThemeBench.php b/performance/benchmarks/ThemeBench.php index 4802ab6..b684d9e 100644 --- a/performance/benchmarks/ThemeBench.php +++ b/performance/benchmarks/ThemeBench.php @@ -4,7 +4,6 @@ use Keepsuit\Liquid\Environment; use Keepsuit\Liquid\Performance\benchmarks\Support\ComplexThemeFixture; -use Keepsuit\Liquid\Template; use PhpBench\Attributes\BeforeMethods; use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; @@ -22,10 +21,6 @@ class ThemeBench { private Environment $environment; - private Template $page; - - private Template $layout; - public function setUp(): void { $this->environment = ComplexThemeFixture::environment(); @@ -33,9 +28,6 @@ public function setUp(): void foreach (ComplexThemeFixture::templateNames() as $name) { $this->environment->parseTemplate($name); } - - $this->page = $this->environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); - $this->layout = $this->environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); } public function benchTokenize(): void @@ -54,16 +46,16 @@ public function benchParse(): void public function benchRender(): void { - $content = $this->page->render(ComplexThemeFixture::newRenderContext($this->environment)); - - $this->layout->render(ComplexThemeFixture::newLayoutRenderContext($this->environment, $content)); + foreach (ComplexThemeFixture::pageTemplateNames() as $templateName) { + ComplexThemeFixture::renderPage($this->environment, $templateName); + } } public function benchStream(): void { - $content = $this->page->stream(ComplexThemeFixture::newRenderContext($this->environment)); - - foreach ($this->layout->stream(ComplexThemeFixture::newLayoutRenderContext($this->environment, $content)) as $chunk) { + foreach (ComplexThemeFixture::pageTemplateNames() as $templateName) { + foreach (ComplexThemeFixture::streamPage($this->environment, $templateName) as $chunk) { + } } } } diff --git a/performance/profile-theme.php b/performance/profile-theme.php index c14eef3..f33de4f 100644 --- a/performance/profile-theme.php +++ b/performance/profile-theme.php @@ -40,10 +40,9 @@ variables: true, )); -$page = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); -$layout = $environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); -$content = $page->render(ComplexThemeFixture::newRenderContext($environment)); -$layout->render(ComplexThemeFixture::newLayoutRenderContext($environment, $content)); +foreach (ComplexThemeFixture::pageTemplateNames() as $templateName) { + ComplexThemeFixture::renderPage($environment, $templateName); +} $report = ProfileReport::fromProfiler($profiler); $json = json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; diff --git a/performance/themes/complex-collection/collection.liquid b/performance/themes/complex-collection/collection.liquid deleted file mode 100644 index fc67740..0000000 --- a/performance/themes/complex-collection/collection.liquid +++ /dev/null @@ -1,10 +0,0 @@ -{% assign product_count = collection.products | size %} -{% if product_count > 0 %} -
- {% render "collection_header", collection: collection %} - {% render "collection_navigation", collection: collection %} - {% render "collection_grid", collection: collection %} -
-{% else %} -

No products found in this collection.

-{% endif %} diff --git a/performance/themes/complex-collection/collection_grid.liquid b/performance/themes/complex-collection/collection_grid.liquid deleted file mode 100644 index eef4ce2..0000000 --- a/performance/themes/complex-collection/collection_grid.liquid +++ /dev/null @@ -1,3 +0,0 @@ -
-{% for product in collection.products %} {% render "product_card", product: product, label: collection.label, position: forloop.index %} -{% endfor %}
diff --git a/performance/themes/complex-collection/collection_navigation.liquid b/performance/themes/complex-collection/collection_navigation.liquid deleted file mode 100644 index 4d7b3e5..0000000 --- a/performance/themes/complex-collection/collection_navigation.liquid +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/performance/themes/complex-collection/layout/theme.liquid b/performance/themes/complex-collection/layout/theme.liquid new file mode 100644 index 0000000..095043a --- /dev/null +++ b/performance/themes/complex-collection/layout/theme.liquid @@ -0,0 +1,12 @@ + + + + {{ shop.name }} — {{ page_title }} + + + + {% render "snippets.shared.site_header", shop: shop, cart: cart, links: linklists.main-menu.links %} + {{ content_for_layout }} + {% render "snippets.shared.site_footer", shop: shop, links: linklists.footer.links %} + + diff --git a/performance/themes/complex-collection/snippets/collection/grid.liquid b/performance/themes/complex-collection/snippets/collection/grid.liquid new file mode 100644 index 0000000..d575a56 --- /dev/null +++ b/performance/themes/complex-collection/snippets/collection/grid.liquid @@ -0,0 +1,3 @@ +
+{% for product in collection.products %} {% render "snippets.product.card", product: product, label: collection.label, position: forloop.index %} +{% endfor %}
diff --git a/performance/themes/complex-collection/collection_header.liquid b/performance/themes/complex-collection/snippets/collection/header.liquid similarity index 66% rename from performance/themes/complex-collection/collection_header.liquid rename to performance/themes/complex-collection/snippets/collection/header.liquid index e521680..9bc83bd 100644 --- a/performance/themes/complex-collection/collection_header.liquid +++ b/performance/themes/complex-collection/snippets/collection/header.liquid @@ -3,4 +3,5 @@

{{ collection.title }}

{{ collection.description }}

{{ collection.products | size }} products selected

+ {% render "snippets.shared.button", label: "Shop all arrivals", url: "/collections/summer-essentials", icon: "arrow", tone: "primary" %} diff --git a/performance/themes/complex-collection/snippets/collection/navigation.liquid b/performance/themes/complex-collection/snippets/collection/navigation.liquid new file mode 100644 index 0000000..7ebdacf --- /dev/null +++ b/performance/themes/complex-collection/snippets/collection/navigation.liquid @@ -0,0 +1,6 @@ + diff --git a/performance/themes/complex-collection/snippets/index/featured_products.liquid b/performance/themes/complex-collection/snippets/index/featured_products.liquid new file mode 100644 index 0000000..3868294 --- /dev/null +++ b/performance/themes/complex-collection/snippets/index/featured_products.liquid @@ -0,0 +1,6 @@ + diff --git a/performance/themes/complex-collection/snippets/index/hero.liquid b/performance/themes/complex-collection/snippets/index/hero.liquid new file mode 100644 index 0000000..1b60169 --- /dev/null +++ b/performance/themes/complex-collection/snippets/index/hero.liquid @@ -0,0 +1,6 @@ +
+

Field notes / {{ collection.label | upcase }}

+

{{ collection.title }}

+

{{ collection.description }}

+ {% render "snippets.shared.button", label: "Explore the collection", url: "/collections/summer-essentials", icon: "arrow", tone: "primary" %} +
diff --git a/performance/themes/complex-collection/snippets/index/journal.liquid b/performance/themes/complex-collection/snippets/index/journal.liquid new file mode 100644 index 0000000..9ce3c3d --- /dev/null +++ b/performance/themes/complex-collection/snippets/index/journal.liquid @@ -0,0 +1,6 @@ +
+

From the journal

Better everyday rituals

+
+{% for article in articles %}

{{ article.title }}

{{ article.excerpt }}

{% render "snippets.shared.button", label: "Read article", url: "/blogs/journal", icon: "arrow", tone: "text" %}
+{% endfor %}
+
diff --git a/performance/themes/complex-collection/snippets/page/content.liquid b/performance/themes/complex-collection/snippets/page/content.liquid new file mode 100644 index 0000000..d777473 --- /dev/null +++ b/performance/themes/complex-collection/snippets/page/content.liquid @@ -0,0 +1,4 @@ +
+

{{ page.content }}

+ {% render "snippets.shared.button", label: "Contact us", url: "/pages/contact", icon: "arrow", tone: "secondary" %} +
diff --git a/performance/themes/complex-collection/snippets/page/header.liquid b/performance/themes/complex-collection/snippets/page/header.liquid new file mode 100644 index 0000000..cc162b5 --- /dev/null +++ b/performance/themes/complex-collection/snippets/page/header.liquid @@ -0,0 +1,4 @@ +
+

{% render "snippets.shared.icon", name: "arrow" %} Northstar journal

+

{{ page.title }}

+
diff --git a/performance/themes/complex-collection/product_card.liquid b/performance/themes/complex-collection/snippets/product/card.liquid similarity index 56% rename from performance/themes/complex-collection/product_card.liquid rename to performance/themes/complex-collection/snippets/product/card.liquid index 9a56c60..6251020 100644 --- a/performance/themes/complex-collection/product_card.liquid +++ b/performance/themes/complex-collection/snippets/product/card.liquid @@ -1,9 +1,9 @@
- {% render "product_media", product: product %} + {% render "snippets.product.media", product: product %}

{{ product.vendor | upcase }}

{{ product.title }}

- {% render "product_pricing", product: product %} - {% render "product_metadata", product: product, label: label %} + {% render "snippets.product.pricing", product: product %} + {% render "snippets.product.metadata", product: product, label: label %}
diff --git a/performance/themes/complex-collection/snippets/product/detail.liquid b/performance/themes/complex-collection/snippets/product/detail.liquid new file mode 100644 index 0000000..f3b9a88 --- /dev/null +++ b/performance/themes/complex-collection/snippets/product/detail.liquid @@ -0,0 +1,10 @@ +
+ {% render "snippets.product.media", product: product %} +
+

{{ product.vendor | upcase }}

+

{{ product.title }}

+ {% render "snippets.product.pricing", product: product %} + {% render "snippets.product.metadata", product: product, label: "featured item" %} + {% render "snippets.shared.button", label: "Add to cart", url: "/cart/add", icon: "bag", tone: "primary" %} +
+
diff --git a/performance/themes/complex-collection/product_media.liquid b/performance/themes/complex-collection/snippets/product/media.liquid similarity index 100% rename from performance/themes/complex-collection/product_media.liquid rename to performance/themes/complex-collection/snippets/product/media.liquid diff --git a/performance/themes/complex-collection/product_metadata.liquid b/performance/themes/complex-collection/snippets/product/metadata.liquid similarity index 100% rename from performance/themes/complex-collection/product_metadata.liquid rename to performance/themes/complex-collection/snippets/product/metadata.liquid diff --git a/performance/themes/complex-collection/product_pricing.liquid b/performance/themes/complex-collection/snippets/product/pricing.liquid similarity index 57% rename from performance/themes/complex-collection/product_pricing.liquid rename to performance/themes/complex-collection/snippets/product/pricing.liquid index 47219fe..59ebdbd 100644 --- a/performance/themes/complex-collection/product_pricing.liquid +++ b/performance/themes/complex-collection/snippets/product/pricing.liquid @@ -1,5 +1,5 @@

- {{ product.price_cents | fixture_money }} - {% if product.compare_at_price_cents > product.price_cents %}{{ product.compare_at_price_cents | fixture_money }}{% endif %} + {% render "snippets.shared.price", cents: product.price_cents %} + {% if product.compare_at_price_cents > product.price_cents %}{% render "snippets.shared.price", cents: product.compare_at_price_cents %}{% endif %} {{ product.compare_at_price_cents | minus: product.price_cents | fixture_money }}

diff --git a/performance/themes/complex-collection/snippets/shared/button.liquid b/performance/themes/complex-collection/snippets/shared/button.liquid new file mode 100644 index 0000000..f8724ec --- /dev/null +++ b/performance/themes/complex-collection/snippets/shared/button.liquid @@ -0,0 +1 @@ +{% render "snippets.shared.icon", name: icon %}{{ label }} diff --git a/performance/themes/complex-collection/snippets/shared/icon.liquid b/performance/themes/complex-collection/snippets/shared/icon.liquid new file mode 100644 index 0000000..81b4ff2 --- /dev/null +++ b/performance/themes/complex-collection/snippets/shared/icon.liquid @@ -0,0 +1,3 @@ +{% if name == "arrow" %}{% endif %} +{% if name == "bag" %}{% endif %} +{% if name == "filter" %}{% endif %} diff --git a/performance/themes/complex-collection/snippets/shared/price.liquid b/performance/themes/complex-collection/snippets/shared/price.liquid new file mode 100644 index 0000000..296b1f8 --- /dev/null +++ b/performance/themes/complex-collection/snippets/shared/price.liquid @@ -0,0 +1 @@ +{{ cents | fixture_money }} diff --git a/performance/themes/complex-collection/snippets/shared/site_footer.liquid b/performance/themes/complex-collection/snippets/shared/site_footer.liquid new file mode 100644 index 0000000..2b2b7c9 --- /dev/null +++ b/performance/themes/complex-collection/snippets/shared/site_footer.liquid @@ -0,0 +1,6 @@ +
+

All prices are in {{ shop.currency }}.

+ +
diff --git a/performance/themes/complex-collection/snippets/shared/site_header.liquid b/performance/themes/complex-collection/snippets/shared/site_header.liquid new file mode 100644 index 0000000..2a5c7e7 --- /dev/null +++ b/performance/themes/complex-collection/snippets/shared/site_header.liquid @@ -0,0 +1,9 @@ + diff --git a/performance/themes/complex-collection/templates/collection.liquid b/performance/themes/complex-collection/templates/collection.liquid new file mode 100644 index 0000000..aba3ae6 --- /dev/null +++ b/performance/themes/complex-collection/templates/collection.liquid @@ -0,0 +1,10 @@ +{% assign product_count = collection.products | size %} +{% if product_count > 0 %} +
+ {% render "snippets.collection.header", collection: collection %} + {% render "snippets.collection.navigation", collection: collection %} + {% render "snippets.collection.grid", collection: collection %} +
+{% else %} +

No products found in this collection.

+{% endif %} diff --git a/performance/themes/complex-collection/templates/index.liquid b/performance/themes/complex-collection/templates/index.liquid new file mode 100644 index 0000000..6b019cf --- /dev/null +++ b/performance/themes/complex-collection/templates/index.liquid @@ -0,0 +1,5 @@ +
+ {% render "snippets.index.hero", collection: collection %} + {% render "snippets.index.featured_products", collection: collection %} + {% render "snippets.index.journal", articles: articles %} +
diff --git a/performance/themes/complex-collection/templates/page.liquid b/performance/themes/complex-collection/templates/page.liquid new file mode 100644 index 0000000..7c7cfb2 --- /dev/null +++ b/performance/themes/complex-collection/templates/page.liquid @@ -0,0 +1,4 @@ +
+ {% render "snippets.page.header", page: page %} + {% render "snippets.page.content", page: page %} +
diff --git a/performance/themes/complex-collection/templates/product.liquid b/performance/themes/complex-collection/templates/product.liquid new file mode 100644 index 0000000..e91566b --- /dev/null +++ b/performance/themes/complex-collection/templates/product.liquid @@ -0,0 +1,3 @@ +
+ {% render "snippets.product.detail", product: product %} +
diff --git a/performance/themes/complex-collection/theme.liquid b/performance/themes/complex-collection/theme.liquid deleted file mode 100644 index a988084..0000000 --- a/performance/themes/complex-collection/theme.liquid +++ /dev/null @@ -1,33 +0,0 @@ - - - - {{ shop.name }} — {{ page_title }} - - - - -
- - {{ content_for_layout }} -
-
-

All prices are in {{ shop.currency }}.

- -
- - diff --git a/tests/Integration/Performance/ComplexThemeFixtureTest.php b/tests/Integration/Performance/ComplexThemeFixtureTest.php index 4135141..0bfc9fd 100644 --- a/tests/Integration/Performance/ComplexThemeFixtureTest.php +++ b/tests/Integration/Performance/ComplexThemeFixtureTest.php @@ -2,25 +2,34 @@ use Keepsuit\Liquid\Performance\benchmarks\Support\ComplexThemeFixture; -test('complex theme fixture renders the deterministic collection page', function () { +test('complex theme fixture renders every page through the shared layout', function () { $environment = ComplexThemeFixture::environment(); - $page = $environment->parseTemplate(ComplexThemeFixture::rootTemplateName()); - $layout = $environment->parseTemplate(ComplexThemeFixture::LAYOUT_TEMPLATE_NAME); + expect(ComplexThemeFixture::templateNames()) + ->toContain('layout.theme') + ->toContain('templates.index') + ->toContain('templates.collection') + ->toContain('templates.product') + ->toContain('templates.page') + ->toContain('snippets.shared.button') + ->toContain('snippets.product.card'); - $content = $page->render(ComplexThemeFixture::newRenderContext($environment)); - $rendered = $layout->render(ComplexThemeFixture::newLayoutRenderContext($environment, $content)); + foreach ([ + 'templates.index' => 'Better everyday rituals', + 'templates.collection' => '24 products selected', + 'templates.product' => 'Weekend Bag', + 'templates.page' => 'Northstar journal', + ] as $templateName => $expectedContent) { + $rendered = ComplexThemeFixture::renderPage($environment, $templateName); + $streamed = implode('', iterator_to_array( + ComplexThemeFixture::streamPage($environment, $templateName), + false, + )); - $streamedContent = $page->stream(ComplexThemeFixture::newRenderContext($environment)); - $streamed = implode('', iterator_to_array( - $layout->stream(ComplexThemeFixture::newLayoutRenderContext($environment, $streamedContent)), - false, - )); - - expect($rendered) - ->toContain('Northstar Goods — Summer Essentials') - ->toContain('24 products selected') - ->toContain('data-handle="sketchbook"') - ->and(hash('sha256', $rendered))->toBe('52a906d56a26957edbf34fc85ed8dc76e3ccb74f9aa012effd5fd84de5e732eb') - ->and($streamed)->toBe($rendered); + expect($rendered) + ->toContain(' diff --git a/tests/Integration/Performance/ComplexThemeFixtureTest.php b/tests/Integration/Performance/ComplexThemeFixtureTest.php index 0bfc9fd..df72e5d 100644 --- a/tests/Integration/Performance/ComplexThemeFixtureTest.php +++ b/tests/Integration/Performance/ComplexThemeFixtureTest.php @@ -28,6 +28,9 @@ expect($rendered) ->toContain('