Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Fusion performance optimization (lazy Component props) #2738

Merged
merged 14 commits into from Apr 22, 2020
Merged
24 changes: 10 additions & 14 deletions Neos.Fusion/Classes/FusionObjects/ComponentImplementation.php
Expand Up @@ -11,6 +11,7 @@
* source code.
*/

use Neos\Fusion\FusionObjects\Helpers\LazyProps;

/**
* A Fusion Component-Object
Expand Down Expand Up @@ -51,37 +52,32 @@ public function evaluate()
* @param array $context
* @return array
*/
protected function prepare($context)
protected function prepare(array $context): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Is this supposed to be extended/inherited and hence the added typehints to be considered breaking?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not @api and no intended extension point. We only seperated this into a special method so we can address this in aspects seperately.

So for me this is ok.

{
$context['props'] = $this->getProps();
$context['props'] = $this->getProps($context);
return $context;
}

/**
* Calculate the component props
*
* @return array
* @param array $context
* @return \ArrayAccess
*/
protected function getProps()
protected function getProps(array $context): \ArrayAccess
{
$sortedChildFusionKeys = $this->sortNestedFusionKeys();
$props = [];
foreach ($sortedChildFusionKeys as $key) {
try {
$props[$key] = $this->fusionValue($key);
} catch (\Exception $e) {
$props[$key] = $this->runtime->handleRenderingException($this->path . '/' . $key, $e);
}
}
$props = new LazyProps($this, $this->path, $this->runtime, $sortedChildFusionKeys, $context);
return $props;
}

/**
* Evaluate the renderer with the give context and return
*
* @param $context
* @param array $context
* @return mixed
*/
protected function render($context)
protected function render(array $context)
{
$this->runtime->pushContextArray($context);
$result = $this->runtime->render($this->path . '/renderer');
Expand Down
117 changes: 117 additions & 0 deletions Neos.Fusion/Classes/FusionObjects/Helpers/LazyProps.php
@@ -0,0 +1,117 @@
<?php

namespace Neos\Fusion\FusionObjects\Helpers;

use Neos\Flow\Annotations as Flow;
use Neos\Fusion\Core\Runtime;

/**
* @Flow\Proxy(false)
*/
final class LazyProps implements \ArrayAccess, \Iterator
Copy link
Member

@mficzel mficzel Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to add this as LazyEvaluationProxy to the Fusion/Core? That would allow to use it in other places aswell.

As this is no @api yet it also makes sense to think about moving it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it too, but this is rather specific for holding an array of keys and defering the evaluation of these. I'm not sure where and how we would re-use this. By only checking the interfaces in Runtime we always have the chance to extract a common class or something else later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-) i now have a usecase for that. Evaluating only the current page of a multipage form. But since i the fusion-forms shall support Neos 4.3 i will copy that code anyways but thanks for the inspiration.

{

/**
* @var array
*/
private $valueCache = [];

/**
* @var string
*/
private $parentPath;

/**
* @var Runtime
*/
private $runtime;

/**
* Index of keys
*
* @var array
*/
private $keys;

/**
* @var object
*/
private $fusionObject;

/**
* @var array
*/
private $effectiveContext;

public function __construct(
object $fusionObject,
string $parentPath,
Runtime $runtime,
array $keys,
array $effectiveContext
) {
$this->fusionObject = $fusionObject;
$this->parentPath = $parentPath;
$this->runtime = $runtime;
$this->keys = array_flip($keys);
$this->effectiveContext = $effectiveContext;
}

public function offsetExists($path)
{
return isset($this->keys[$path]);
}

public function offsetGet($path)
{
if (!isset($this->valueCache[$path])) {
$this->runtime->pushContextArray($this->effectiveContext);
try {
$this->valueCache[$path] = $this->runtime->evaluate($this->parentPath . '/' . $path,
$this->fusionObject);
} finally {
$this->runtime->popContext();
}
}
return $this->valueCache[$path];
}

public function offsetSet($path, $value)
{
// NOOP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't that throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the "set call" should throw an exception? Yes, we could do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think trying to set a lazy prop should throw :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's do that IMO. Otherwise looks good :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albe It's a good idea to resolve all conversations before hitting the merge button. Otherwise open discussions might get lost – like this one.. :)

}

public function offsetUnset($path)
{
// NOOP
}

public function current()
{
$path = key($this->keys);
if ($path === null) {
return null;
}
return $this->offsetGet($path);
}

public function next()
{
next($this->keys);
}

public function key()
{
return key($this->keys);
}

public function valid()
{
return current($this->keys) !== false;
}

public function rewind()
{
reset($this->keys);
}
}
10 changes: 10 additions & 0 deletions Neos.Fusion/Tests/Functional/FusionObjects/ComponentTest.php
Expand Up @@ -56,4 +56,14 @@ public function componentSandboxRenderer()
$view->setFusionPath('component/sandboxRenderer');
self::assertEquals('Hello ', $view->render());
}

/**
* @test
*/
public function componentLazyRenderer()
{
$view = $this->buildView();
$view->setFusionPath('component/lazyRenderer');
self::assertEquals('Hello', $view->render());
}
}
Expand Up @@ -40,3 +40,11 @@ component.sandboxRenderer = Neos.Fusion:Component {
}
}
}

component.lazyRenderer = Neos.Fusion:Component {
a = 'Hello'
// This prop is not evaluated, since it is not used - otherwise the test would fail
b = Neos.Fusion:NotImplemented

renderer = ${props.a}
}