Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions performance/benchmarks/OperationBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ class OperationBench

private Template $productListTemplate;

private Template $conditionTemplate;

private Template $assignTemplate;

private Template $assignCompositeTemplate;

private Template $captureTemplate;

/** @var array<string, mixed> */
private array $assignCompositeData;

/**
* Built in setUp, not in the subject: these two subjects measure how
* Drop::__get resolves a property, and constructing a ProductDrop (variants,
Expand All @@ -64,6 +75,15 @@ public function setUp(): void
$this->dropMethodMissingHitTemplate = $this->environment->parseString(str_repeat('{{ product.metafields.material }}', 64));
$this->dropMethodMissingMissTemplate = $this->environment->parseString(str_repeat('{{ product.metafields.unknown }}', 64));
$this->productListTemplate = $this->environment->parseString('{% for product in products %}{{ product.title }}{% endfor %}');
$this->conditionTemplate = $this->environment->parseString(str_repeat('{% if value == expected %}x{% endif %}', 64));
$this->assignTemplate = $this->environment->parseString(str_repeat('{% assign value = value %}', 64));
$this->assignCompositeTemplate = $this->environment->parseString(str_repeat('{% assign value = values %}', 16));
$this->captureTemplate = $this->environment->parseString(str_repeat('{% capture value %}captured{% endcapture %}', 64));
$this->assignCompositeData = [
'title' => 'Product title',
'tags' => ['one', 'two', 'three'],
'metadata' => ['material' => 'cotton', 'color' => 'blue'],
];
$this->productDrop = Database::product();
$this->productList = Database::products();
}
Expand Down Expand Up @@ -164,6 +184,33 @@ public function benchFilterWithArguments(): void
$this->filterWithArgumentsTemplate->render($this->filterContext());
}

public function benchConditionRender(): void
{
$this->conditionTemplate->render($this->environment->newRenderContext(staticData: [
'value' => 'value',
'expected' => 'value',
]));
}

public function benchAssignRender(): void
{
$this->assignTemplate->render($this->environment->newRenderContext(staticData: [
'value' => 'value',
]));
}

public function benchAssignCompositeRender(): void
{
$this->assignCompositeTemplate->render($this->environment->newRenderContext(staticData: [
'values' => $this->assignCompositeData,
]));
}

public function benchCaptureRender(): void
{
$this->captureTemplate->render($this->environment->newRenderContext());
}

/**
* @param \Generator<string> $stream
*/
Expand Down
13 changes: 11 additions & 2 deletions performance/benchmarks/ThemeBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,17 @@ public function benchRender(): void
public function benchStream(): void
{
foreach ($this->pageTemplateNames as $pageTemplateName) {
foreach (StorefrontTheme::streamPage($this->environment, $pageTemplateName) as $chunk) {
}
$this->drain(StorefrontTheme::streamPage($this->environment, $pageTemplateName));
}
}

/**
* @param \Generator<string> $stream
*/
private function drain(\Generator $stream): void
{
while ($stream->valid()) {
$stream->next();
}
}
}
4 changes: 3 additions & 1 deletion src/Condition/Condition.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class Condition implements HasParseTreeVisitorChildren

protected ?Condition $childCondition = null;

protected ?ConditionOperator $parsedOperator = null;

public ?BodyNode $body = null;

public function __construct(
Expand Down Expand Up @@ -110,7 +112,7 @@ protected function interpretCondition(mixed $left, mixed $right, ?string $operat
return (bool) static::$customOperators[$operator]($left, $right);
}

return ConditionOperator::parse($operator)->evaluate($left, $right);
return ($this->parsedOperator ??= ConditionOperator::parse($operator))->evaluate($left, $right);
}

protected function toLiquidValue(mixed $value): mixed
Expand Down
44 changes: 32 additions & 12 deletions src/Nodes/BodyNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

class BodyNode extends Node implements CanBeStreamed
{
private const MAX_BUFFERED_BYTES = 4096;

public function __construct(
/** @var array<Node> */
protected array $children = [],
Expand Down Expand Up @@ -77,8 +79,6 @@ public function render(RenderContext $context): string
}
}

$context->resourceLimits->incrementWriteScore($output);

return $output;
}

Expand All @@ -91,42 +91,62 @@ public function stream(RenderContext $context): \Generator
{
$context->resourceLimits->incrementRenderScore(count($this->children));

$buffer = '';

foreach ($this->children as $node) {
// Text is the majority of children and cannot fail or interrupt.
if ($node instanceof Text) {
$context->resourceLimits->incrementWriteScore($node->value);
yield $node->value;
$buffer .= $node->value;

continue;
}

if (strlen($buffer) >= self::MAX_BUFFERED_BYTES) {
yield $buffer;
$buffer = '';
}

try {
if ($node instanceof Disableable && $node instanceof Tag) {
$node->ensureTagIsEnabled($context);
}

if ($node instanceof CanBeStreamed) {
foreach ($node->stream($context) as $output) {
$context->resourceLimits->incrementWriteScore($output);
yield $output;
$buffer .= $output;

if (strlen($buffer) >= self::MAX_BUFFERED_BYTES) {
yield $buffer;
$buffer = '';
}
}
} else {
$output = $node->render($context);
$context->resourceLimits->incrementWriteScore($output);
yield $output;
$buffer .= $node->render($context);
}
} catch (UndefinedVariableException|UndefinedDropMethodException|UndefinedFilterException $exception) {
if ($buffer !== '') {
yield $buffer;
$buffer = '';
}

$context->handleError($exception, $node->lineNumber);
} catch (\Throwable $exception) {
$output = $context->handleError($exception, $node->lineNumber);
$context->resourceLimits->incrementWriteScore($output);
yield $output;
if ($buffer !== '') {
yield $buffer;
$buffer = '';
}

$buffer .= $context->handleError($exception, $node->lineNumber);
}

if ($context->hasInterrupt()) {
break;
}
}

if ($buffer !== '') {
yield $buffer;
}
}

public function blank(): bool
Expand Down
2 changes: 2 additions & 0 deletions src/Nodes/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public function render(RenderContext $context): string
}

/**
* @return \Generator<string>
*
* @throws LiquidException
*/
public function stream(RenderContext $context): \Generator
Expand Down
32 changes: 26 additions & 6 deletions src/Render/RenderContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use ArithmeticError;
use Closure;
use Generator;
use Keepsuit\Liquid\Contracts\CanBeEvaluated;
use Keepsuit\Liquid\Contracts\IsContextAware;
use Keepsuit\Liquid\Contracts\LiquidErrorHandler;
Expand Down Expand Up @@ -88,14 +89,19 @@ public function __construct(
public readonly RenderContextOptions $options = new RenderContextOptions,
?ResourceLimits $resourceLimits = null,
?Environment $environment = null,
/**
* Sub-contexts inherit the parent state; building a fresh one here would
* merge the environment registers only to have it replaced.
*/
?ContextSharedState $sharedState = null,
) {
$this->environment = $environment ?? Environment::default();
$this->resourceLimits = $resourceLimits ?? ResourceLimits::clone($this->environment->defaultResourceLimits);
$this->errorHandler = $this->options->rethrowErrors ? new RethrowErrorHandler : $this->environment->errorHandler;

$this->scopes = [[]];

$this->sharedState = new ContextSharedState(
$this->sharedState = $sharedState ?? new ContextSharedState(
staticVariables: $staticData,
registers: array_merge($this->environment->getRegisters(), $registers),
);
Expand Down Expand Up @@ -142,6 +148,21 @@ public function stack(Closure $closure)
return $result;
}

/**
* @param Closure(RenderContext $context): Generator<string> $closure
* @return Generator<string>
*/
public function streamedStack(Closure $closure)
{
$this->push();

try {
yield from $closure($this);
} finally {
$this->pop();
}
}

public function evaluate(mixed $value): mixed
{
while ($value instanceof CanBeEvaluated) {
Expand Down Expand Up @@ -365,10 +386,9 @@ public function setToActiveScope(string $key, mixed $value): array
{
$index = count($this->scopes) - 1;

return $this->scopes[$index] = [
...$this->scopes[$index],
$key => $value,
];
$this->scopes[$index][$key] = $value;

return $this->scopes[$index];
}

public function pushInterrupt(Interrupt $interrupt): void
Expand Down Expand Up @@ -458,9 +478,9 @@ public function newIsolatedSubContext(?string $templateName = null, ?RenderConte
options: $options ?? $this->options,
resourceLimits: $this->resourceLimits,
environment: $this->environment,
sharedState: $this->sharedState,
);
$subContext->baseScopeDepth = $this->baseScopeDepth + 1;
$subContext->sharedState = $this->sharedState;
$subContext->templateName = $templateName;
$subContext->partial = true;

Expand Down
Loading