Problem

Drupal\Core\Template\Attribute has become a bit of a God class with too many responsibilities. Move CSS Class parsing code off to its own class and apply some speed improvements to the logic of the class.

Issue History Note

This issue ticket was began over two years ago with the Twig rendering system. There are references in the older patches to Twig Environment and the then concurrently being developed assertion system. As of the most recent patch these references are removed. This issue thread is maintained for needed context for the code reviewers. The remaining Twig issues have their own issues.

<?php

use Drupal\Core\Template\Attribute;

require_once 'autoload.php';

function providerTestAttributeData() {
  return [
    ['&"\'<>' => 'value'],
    ['title' => '&"\'<>'],
    ['class' => ['first', 'last']],
    ['disabled' => TRUE],
    ['disabled' => FALSE],
    ['alt' => ''],
    ['alt' => NULL],
    [
      'id' => 'id-test',
      'class' => ['first', 'last'],
      'alt' => 'Alternate',
    ],
    [],
  ];
}

$startTime = microtime(true);
// Your content to test
$attributesData = providerTestAttributeData();
for ($i=0; $i < 10000; $i++) {
  foreach ($attributesData as  $attributes) {
    // Convert array to attribute object.
    $attribute = new Attribute($attributes);
    // Change Attribute.
    $attribute->addClass(array('orange', 'blue'));
    $attribute->removeAttribute('id');
    // Cast to string.
    $value = (string) $attribute;
  }
}
$endTime = microtime(true);
$elapsed = $endTime - $startTime;

echo "Execution time : $elapsed seconds";
CommentFileSizeAuthor
#126 2444003-126.patch17.04 KBMerryHamster
#120 interdiff.diff4.69 KBAki Tendo
#120 2444003-120.patch17.2 KBAki Tendo
#118 2444003-118.patch18.29 KBAki Tendo
#113 interdiff2.diff690 bytesAki Tendo
#112 2444003-112.patch12.82 KBAki Tendo
#107 Home___Site-Install-without_patch.png45.86 KBjoelpittet
#107 Home___Site-Install_-_with_patch.png101.44 KBjoelpittet
#104 interdiff.diff7.04 KBAki Tendo
#104 2444003-104.patch12.72 KBAki Tendo
#102 2444003-102.patch8.38 KBAki Tendo
#98 2444003-98.patch35.23 KBAki Tendo
#96 2444003-96.patch77.72 KBAki Tendo
#94 2444003-94.patch76.56 KBAki Tendo
#92 2444003-92.patch57.34 KBAki Tendo
#89 2444003-90.patch55.94 KBAki Tendo
#88 2444003-88.patch117.86 KBAki Tendo
#86 2444003-86.patch71.94 KBAki Tendo
#83 template_attribute_optimize_2444003-83.patch71.94 KBAki Tendo
#81 template_attribute_optimize_2444003-81.patch69.07 KBAki Tendo
#77 template_attribute_optimize_2444003-77.patch34.63 KBAki Tendo
#63 assertions_for_twig-2444003-63.patch8.93 KBborisson_
#60 2444003-59.diff8.89 KBAki Tendo
#59 2444003-59.diff8.89 KBAki Tendo
#55 interdiff-55.txt2.91 KBAki Tendo
#55 2444003-55.diff32.78 KBAki Tendo
#51 2444003-50.diff11.22 KBAki Tendo
#48 2444003-48.diff67.82 KBAki Tendo
#48 asserts-only--2444003-48--do-not-test.diff13.31 KBAki Tendo
#46 progress--2444003.diff131.01 KBAki Tendo
#42 interdiff-template-section-only--244003-42--do-not-test.diff6.1 KBAki Tendo
#42 template-assertions-only--244003-42--do-not-test.diff72.54 KBAki Tendo
#42 testable--244003-42.diff118.7 KBAki Tendo
#36 do-not-test-2444003-36.diff70.73 KBAki Tendo
#36 2444003-36.patch112.49 KBAki Tendo
#31 interdiff-29-31.txt6.34 KBAki Tendo
#31 2444003-31.diff86.57 KBAki Tendo
#29 interdiff-21-29.txt21.95 KBAki Tendo
#29 2444003-29.diff84.48 KBAki Tendo
#28 patch.diff168 bytesAki Tendo
#26 interdiff.txt86.95 KBAki Tendo
#26 2444003-21.diff70.62 KBAki Tendo
#20 2444003-20.diff74.78 KBAki Tendo
#18 2444003-18.diff74.68 KBAki Tendo
#15 2444003-15.diff74.73 KBAki Tendo
#10 interdiff-Template_Assertions-first_to_second_draft-2444003-10.txt665 bytesAki Tendo
#10 Template_Assertions-second_draft-2444003-10.diff7 KBAki Tendo
#6 Template_Assertions-first_draft-2444003-6.diff6.88 KBAki Tendo
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett’s picture

Per #2408013-19: Adding Assertions to Drupal - Test Tools., I think this should be rolled back into the parent issue, and this should be closed.

Aki Tendo’s picture

Disagree because just as classes must not try to do too much, so too should individual tasks. The Assertion Handling ticket deals specifically with the resolution of the assertions. The Assertion tickets themselves, like this one, may end up numbering well over a hundred. We're talking about going over the entire Drupal 8 codebase over the next few years and finding spots where it will help our code development to have an assert statement. If all that material was bound to the Assertion Handling ticket that ticket would never be completed.

The scope of this may need to be expanded to handle the whole Template Core library though. An individual class likely is too specific, but I'm in a baby steps mentality.

tim.plunkett’s picture

I'm asking for ONE (1) conversion to be made as part of the parent issue.

Aki Tendo’s picture

Ah. Makes sense. A good candidate for that would be the boot assertions.

Aki Tendo’s picture

Title: TwigExtension Assertions » Template Assertions
Aki Tendo’s picture

Status: Active » Needs review
FileSize
6.88 KB

The template assertions and the explanation of the services.yml snafu that inspired this whole project.

This patch can (and should) be tested without its parent however, if it fails it may be something other than the patch at fault. All I've done is insert assertion statements based on the PHPdoc lines and the assertions regarding the url and link generators. The testers run in ASSERT_WARN mode, and if they trip then either something is wrong with the test, or something is wrong with the code outside the assertion (that I'm not qualified to fix), or the PHP doc statements are wrong and least likely is the assertion is wrong.

Without it's parent there is no way for these assertions to become active and be trapped in actual use.

Again, these materials are here off in their own file to keep the parent from becoming monstrous in size.

Aki Tendo’s picture

Issue summary: View changes

Updated issue summary to conform with template.

Status: Needs review » Needs work

The last submitted patch, 6: Template_Assertions-first_draft-2444003-6.diff, failed testing.

Aki Tendo’s picture

The correction of these fails is a separate issue and will be reported as such. The assertion in question is working as designed assuming the php doc is correct.

Aki Tendo’s picture

This includes the patch at the tail of this ticket..

https://www.drupal.org/node/2449741

That fulfills both the failed tests and the assertion.

YesCT’s picture

Issue summary: View changes
Issue tags: +Needs issue summary update

needs a beta evaluation (see instructions in the issue summary under the remaining tasks section) especially since this is a normal task and at risk for not being commitable.

#2449741: Drupal\Core\Template\Attribute->createAttributeValue does not always return instance of \Drupal\Core\Template\AttributeValueBase was marked duplicate of this. but it had test additions. I dont see those here. why not?

Aki Tendo’s picture

Issue summary: View changes

Issue summary updated as requested.

Aki Tendo’s picture

Issue summary: View changes
Aki Tendo’s picture

Aki Tendo’s picture

FileSize
74.73 KB

Lot of changes, need to get a handle on what breaks on a system wide test. This patch is not up to snuff comments or code formatting wide - I just need to see how the test bots handle it. Also note - this particular patch is combined with it's parent. I'm unsure if I want to keep them divided any longer.

Status: Needs review » Needs work

The last submitted patch, 15: 2444003-15.diff, failed testing.

Aki Tendo’s picture

Issue summary: View changes

Added Beta eval.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
74.68 KB

Last patch had a require statement I was using to pull var-dumper into PHP unit test I forgot to drop...

Status: Needs review » Needs work

The last submitted patch, 18: 2444003-18.diff, failed testing.

Aki Tendo’s picture

FileSize
74.78 KB

Couple of fixes which locally look to be accounting for a least 3/4 of the errors - a bad regex and a failure to init the class array.

Aki Tendo’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 20: 2444003-20.diff, failed testing.

star-szr’s picture

Interdiffs would be great for these @Aki Tendo for those following along at home :)

Aki Tendo’s picture

The last two I've tried to make had so many whitespace switchout notices I could understand what they meant and I wrote the code. The next patch I push will be properly formatted and will be the last one without an interdiff against previous patches, I swear. It will have an interdiff against itself and it's test code.

I'm not at a patch release point tonight as I end work, but this is what has been added.

1. TWIG. That should make themers happy. For the moment it is using its own internal templates and it starts a raw copy of twig with no Drupal extensions whatsoever. I plan to allow it to use the current theme's templates if I can accurately determine it is safe to do so. That or use an independent means of informing the fault system on were to find errors. Input on which to do appreciated.

2 Namespace changed. From Drupal\Core\Error to Drupal\Core\Fault Since "Error" refers to PHP's older trigger_error system it's a misleading name for the system. Fault however can apply to any of the three types.

3 Symfony Responder and Request are now used to compose the handler responses for consistency with the rest of the system. Note this means the fault system will require a clean copy of Symfony to work. It is not perturbed by issues in Drupal though. The assertion callback is now in a child class of Symfony's Response class.

4. The core of the parent system will be in three classes. Except for minor changes and additions, they are done.

# Additions I must make
I must get the system to close gracefully when Twig has a compile error against its own files.
Extensions to the unit tests

  1. PHP Unit, as of the last test, can check assertions. Simpletest needs the same functionality so that clean tests can be written.

# Additions I'm mulling over as feasable in time for Drupal 8.0.x release

Translation file loading - this will work by simply looking in sites/default for a faults.yml file (formerly error.yml). That file can be in any language desired, and it's responses override those found elsewhere in the system. A method will be exposed that will be callable from the settings.php file to point the FaultHandler at a different local faults.yml file.

INPUT NEEDED!! Deprecation declaration - I know how to modify Drupal's base unit test class in PHPUnit and I believe simpletest to ignore trigger_error(' ', E_USER_DEPRECATED). This would allow deprecation throws to be put onto those methods scheduled for removal. The main ramification of this is production sites would need to run as E_ALL & ~E_USER_DEPRECATED instead of just E_ALL. Likewise the autotesters would have to be set up to note deprecation throws but not fail the test over them. Also, instead of coming up with error messages for these, the message string for them would be the projected removing version - for example

trigger_error('8.0rc1', E_USER_DEPRECATED)

The @deprecated php comment lets us know what's deprecated, but it doesn't let us know who's calling it. Clearing the way for that error throw solves this problem. Then again, maybe that should just get it's own ticket. Thoughts?

Aki Tendo’s picture

I'm to the unit tests now, other problems are resolved - I think - though I won't *know* until the tests are running.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
70.62 KB
86.95 KB

More changes

* Unrelated code from the Fault System library removed from this patch. What remains is what is needed by this patch's unit tests.
* The Attribute class has been converted to an array object - it was already implementing all of the interfaces. PHP's internal implementation of Array Object will run faster than the homemade one that has been replaced. The storage method now merely wraps copyArray to maintain BC.
* Class name validity assertion moved from the ValueBase class to Attribute.
* Null can now be passed to an attribute.
* Code formatting applied.

I was still having trouble with some tests locally unrelated - but since those same tests fail when I checkout head locally I'm going to see if it's just a local problem.

Status: Needs review » Needs work

The last submitted patch, 26: 2444003-21.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
168 bytes

Checking something. This patch should pass. If it don't, head is broken.

Aki Tendo’s picture

Priority: Normal » Major
FileSize
84.48 KB
21.95 KB

This should pass. The problem was the test suite requires XHTML attribute format to validate.

Status: Needs review » Needs work

The last submitted patch, 29: 2444003-29.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
86.57 KB
6.34 KB

The tests that failed did so because they where checking for HTML5 style boolean attributes. The Attribute system cannot reliably be asked to provide alternating formats, and replacing XPath promises to be more difficult so the values these tests are looking for as been changed to XHTML.

Aki Tendo’s picture

Title: Template Assertions » Drupal\Core\Template\Attribute revision to smooth out class attribute handling, optimize code, add assertions to all classes in the namespace where needed.

Better title added.

Reminder, the patch above contains enough dupe code from the parent ticket to pass its own unit tests since those tests cannot be conducted without that supporting code.

star-szr’s picture

@Aki Tendo much better title! :) To help patch reviewers, usually what folks do in cases like this is upload a second patch without the changes in the parent issue, ending with -do-not-test.patch to skip automated testing.

Aki Tendo’s picture

But that would be patch 29. Should I re-upload it with that label?

star-szr’s picture

@Aki Tendo no need, but in my opinion it needs to be clearer what you intend to get committed here versus on the parent issue. Basically reviewers don't generally like to review "dupe code", but from your comment I was initially under the impression that this patch also contained all the code from the parent issue, but it only contains some I guess? That feels messy in terms of issue/patch management.

The key takeaways I'm trying to get across are:

1. We need to be able to keep straight which code should live in which issue, because as things start to get committed we should try and avoid commit conflicts. I'm not saying you aren't doing this now, but from my perspective it's a bit opaque.
2. Others (especially reviewers, but if you hope to have collaborators as well!) should be aware via your issue comments when you are making patches combining code from multiple issues, and coming back to #1, we need to know how to strip down a patch to only the changes for this issue so that we can as a community review and eventually commit that code without its parent "dupe code".

Hope that makes some sense, just don't want this to get stopped up. Back to your regularly scheduled programming.

Aki Tendo’s picture

FileSize
112.49 KB
70.73 KB

Updated the template patch to reflect the changes in its parent and the 7.0 compat changes which make the previous patch unappliable. As mentioned previously I had a git crash which lost my change record so I can't do an interdiff with the previous patch.

The do-not-test patch contains just the code relevant to this ticket. It isn't testable on its own though - the testing ticket includes the parent issue.

Status: Needs review » Needs work

The last submitted patch, 36: do-not-test-2444003-36.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review

Somehow the do not test patch ran when it wasn't supposed to. Full patch passed.

Fabianx’s picture

+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -57,26 +53,20 @@ class Attribute implements \ArrayAccess, \IteratorAggregate {
+    foreach ($attributes as $name => $value) {
+      $this[$name] = $value;
     }

nit - exchangeArray() is way faster than setting every data via the set() calls (though most objects we create are empty anyway), but +1 to using ArrayObject.

Aki Tendo’s picture

It is faster but it does nothing to the incoming array, and neither does parent::_construct(). Take a closer look at offsetSet

  /**
   * Overrides ArrayObject::offsetSet().
   */
  public function offsetSet($name, $value) {
    parent::offsetSet($name, $this->createAttributeValue($name, $value));
  }

There is a method to my madness up there - by doing that loop I force every value to pass through the validation and transformation performed by createAttributeValue.

Any call set done via $this[$var] = 'foo' will pass through offsetSet magically because of the ArrayAccess Interface.

Fabianx’s picture

#40: Hah, good catch, that was indeed a bug before setting the storage directly ... Thanks!

Aki Tendo’s picture

Ok, new class (And I guess a very minor API addition) added -- AttributeNamedId. This class monitors attributes named Id's, making sure they are of a valid format to be ID's, and making sure they are unique document wide (or in the case of Ajax responses they are at least unique within the dataset being transferred). These assertions will help diagnose certain JavaScript bugs - browser behavior when one or more elements share an ID is undefined.

There are three patches. The one that is being tested includes code from the parent ticket needed for testing to occur. Then there is the main template assertion patch for those just now reading the ticket. For those following along there is an interdiff with the version on post #36.

EDIT: Just noticed I didn't do a code standard sniff. I'll await the results of global unit testing to make that check.

SECOND EDIT: It occurred to me this class has the potential to issue random ID's safely to doc elements. I've seen jQuery and prototype both do this from time to time. Is there a usage scenario for this? The way I would implement this is simply $attribute['id]->randomize() with the function returning the assigned ID. Before I place that logic though I'd like to know a use case for it because at the moment I can't think of any useful application which makes it code bloat. Also, the presence of the feature would force the ID's issued to need to be tracked in production.

Status: Needs review » Needs work

The last submitted patch, 42: testable--244003-42.diff, failed testing.

Aki Tendo’s picture

I've double checked the failure output against several of the pages the fail is reported when the patch is *not* installed - there are indeed multiple pages with repeated id's in the system. Hence the AttributeNamedId class cannot be added to the system until these errors are removed.

A separate issue has been created for this problem.

Aki Tendo’s picture

Status: Needs work » Needs review
Issue tags: -API addition +CSS, +API clean-up

Updating issue tags now that I have a better understanding on what they should be.

Also changing back to Needs Review. While the id issue is decided the previous working patch without AttributeNamedId (#36) can still be committed.

Aki Tendo’s picture

FileSize
131.01 KB

Checking on exactly how many test fails was solved by fixing the simpletest master page id duping. If this works I'll do a more formal interdiff post.

Status: Needs review » Needs work

The last submitted patch, 46: progress--2444003.diff, failed testing.

Aki Tendo’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -CSS, -API clean-up
FileSize
13.31 KB
67.82 KB

As of this post, dividing the ticket. The Attribute work runs deeper than I anticipated as the newbie coming on board, and far out scopes what this ticket was originally meant for - simply adding some asserts to TwigExtension. So the patches attached address just that problem which was the original goal of this ticket. The assertion work will move to a new ticket I'll link up from here when it's ready, since it's massive.

The "asserts-only" is just this ticket's code. The other ticket includes supporting code from the parent ticket.

Aki Tendo’s picture

Title: Drupal\Core\Template\Attribute revision to smooth out class attribute handling, optimize code, add assertions to all classes in the namespace where needed. » Use assert statements to prevent cryptic error throws from the Drupal TwigExtension when modules are misconfigured.

Status: Needs review » Needs work

The last submitted patch, 48: 2444003-48.diff, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
11.22 KB

This is a start over. No assertion aware tests have been added to this ticket yet - they may not be needed since all the assertions are of the very straightforward 'is_a' variety. We do check to see if the urlGenerator is set.

Any assertions that fail on this preliminary search will be assigned their own child tickets and removed from this one. PHP Units have passed.

There is (and will be) no work on the Attribute classes in this ticket anymore.

star-szr’s picture

Status: Needs review » Needs work

That's easier to review :) Thanks @Aki Tendo. Some comments below.

  1. +++ b/core/lib/Drupal/Core/Template/TwigExtension.php
    @@ -276,12 +282,11 @@ public function getLink($text, $url, array $attributes = []) {
    -    $build = [
    +    return [
           '#type' => 'link',
           '#title' => $text,
           '#url' => $url,
         ];
    -    return $build;
    

    This looks out of scope unless it's needed for some reason.

  2. +++ b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php
    @@ -21,14 +21,14 @@ class TwigNodeVisitor implements \Twig_NodeVisitorInterface {
    -  function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    +  public function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    ...
    -  function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    +  public function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    
    @@ -62,7 +62,7 @@ function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    -  function getPriority() {
    +  public function getPriority() {
    

    Same with these changes? Again I may be wrong if they are needed for some reason but otherwise they are bloating the patch.

  3. +++ b/core/lib/Drupal/Core/Template/TwigTransTokenParser.php
    @@ -61,14 +61,14 @@ public function parse(\Twig_Token $token) {
    -  public function decideForFork($token) {
    +  public function decideForFork( \Twig_Token $token) {
    ...
    -  public function decideForEnd($token) {
    +  public function decideForEnd( \Twig_Token $token) {
    

    I think there is an extra space here between the opening parenthesis and \Twig_Token.

Aki Tendo’s picture

1 is just a tiny optimization - I'll roll it back if desired. 2. is a code standards violation - I can't get phpcs to validate the changes unless I do that. 3 I can fix (I hadn't ran phpcs over the file yet.

star-szr’s picture

For #52.2 adding public to the methods is the only change that this patch makes in TwigNodeVisitor (that I can spot, anyway). So the changes to that file should be reverted altogether. Problem solved? I can't tell exactly how you're using phpcs to validate things though :)

Aki Tendo’s picture

FileSize
32.78 KB
2.91 KB

Changes made.

Aki Tendo’s picture

Status: Needs work » Needs review
star-szr’s picture

I guess that's including code from the parent? Not sure how things are broken down. Anyway, interdiff looks good.

Aki Tendo’s picture

Yes. This particular assertion group doesn't need it's parent, but I accidentally merged it in anyway.

Aki Tendo’s picture

Title: Use assert statements to prevent cryptic error throws from the Drupal TwigExtension when modules are misconfigured. » Assertions for Twig including one to prevent cryptic error throws when module services.yml files are miswritten.
Issue summary: View changes
FileSize
8.89 KB

With the parent now in core, this should be a final re-roll. A followup ticket will adjust TwigExtensionTest to actually test the assertions being added here, but those assertion tests require #2536560: Runtime Assertion unit and functional testing to run.

NOTE: The .htaccess file in the patch has the assert flag set to 1 due to an issue with the test bots. This file change should not be included in the final commit to core.

Aki Tendo’s picture

Reuploading patch to force the test bots to eval.

Status: Needs review » Needs work

The last submitted patch, 60: 2444003-59.diff, failed testing.

geertvd’s picture

Issue tags: +Needs reroll
borisson_’s picture

Issue tags: -Needs reroll
FileSize
8.93 KB

Reroll of #60 attached.

Aki Tendo’s picture

I forgot to note in this ticket that I'm splitting it up by class in attempt to speed things along. Should be able to get some work in on this after work (in about 12 hours)

Aki Tendo’s picture

Aki Tendo’s picture

Status: Needs work » Active
star-szr’s picture

Category: Task » Plan
joelpittet’s picture

@Aki Tendo what's the status of this Meta? Should we move to try to do this in 8.1.x or do you think it stands to be achieved in the 8.0.x RC cycle?

Aki Tendo’s picture

Both sub tickets are ready, but Alex Pott stated in IRC he doesn't want to introduce any further assertions during the RC cycle. So 8.0.1 I guess is earliest possible - I dunno for sure.

joelpittet’s picture

Status: Active » Postponed

Ok let's postpone on 8.0.0 release and pick-up after that. Thank you for checking into that.

joelpittet’s picture

Version: 8.0.x-dev » 8.1.x-dev

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Aki Tendo’s picture

Status: Postponed » Active

Both child ticket patches for this work with 8.2.x, so I'm not sure this necessarily needs further postponement - it can probably be worked in.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Aki Tendo’s picture

I'm working on this again, stripping out the disruptive stuff and leaving behind the optimizations.

Aki Tendo’s picture

Ok, new patch. The scope of this patch has been changed to just the optimizations for the Attribute classes. Twig related stuff will be handled off in their own patch.

I massively rewrote the issue summary so please check that.

Aki Tendo’s picture

Title: Assertions for Twig including one to prevent cryptic error throws when module services.yml files are miswritten. » Template\Attribute system optimization (uses \ArrayObject)
Category: Plan » Task
Aki Tendo’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, 77: template_attribute_optimize_2444003-77.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
69.07 KB

Ok, Let's try that again with the Safe Markup branch of createAttributeValue properly implemented. Hopefully this test won't look like an explosion in a mattress factory.

Status: Needs review » Needs work

The last submitted patch, 81: template_attribute_optimize_2444003-81.patch, failed testing.

Aki Tendo’s picture

Dropping AttributeBoolean and AttributeString. The former because it's gross overkill, the Attribute class can handle it with less overhead and lines of code than having a new class. The latter because its misleading - String was the catch all for anything not handled by another class since Attributes are written as string. The logic for this is now in Attribute class.

Render is removed from the interface and all classes. Attribute is responsible for rendering. This allows these helper classes to be ignorant of their names which cleans up the copying code of Attributes between elements.

Aki Tendo’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 83: template_attribute_optimize_2444003-83.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
71.94 KB

Seeing if the file name itself is the reason the patch won't work.

Status: Needs review » Needs work

The last submitted patch, 86: 2444003-86.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
117.86 KB

Trying creating the patch from the command line rather than using sourcetree.

Aki Tendo’s picture

88 is going to fail, so here's 90. Git on Windows puts garbage before and after the diff calls, so I trimmed out the garbage. Maybe this will apply. Stupid Windows.

The last submitted patch, 88: 2444003-88.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 89: 2444003-90.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
57.34 KB

Another try. At least the patch is applying now.

Status: Needs review » Needs work

The last submitted patch, 92: 2444003-92.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
76.56 KB

Perhaps the reason these outside tests are failing is I'm trying to refactor too much. So the AttributeValueInterfaces for all not null objects are back in place.

Status: Needs review » Needs work

The last submitted patch, 94: 2444003-94.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
77.72 KB

Attribute must be able to accept objects with ArrayAccess.

Status: Needs review » Needs work

The last submitted patch, 96: 2444003-96.patch, failed testing.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
35.23 KB

Again.

Status: Needs review » Needs work

The last submitted patch, 98: 2444003-98.patch, failed testing.

joelpittet’s picture

@Aki Tendo, just a tip, don't take it as a must do, but there is a issue queue trick I've seen some people do when trying to fire the testbot with lots of patches is to create a patch ignore issue set to Priority minor and dump all their tests in it. It's a show your work vs not thing I guess.

Here's one of mine for example: https://www.drupal.org/node/2465399
And all the other ones
https://www.drupal.org/project/issues/search/drupal?text=ignore&assigned...

joelpittet’s picture

You're getting close on this, but I'm a bit afraid of the changes in there that change the tests which gives a code smell of API change which is much harder to make a case for and could break people's sites. Also the booleanAttributes change in there seems to limit the scope of the boolean attributes to HTML when this could be used for XML as it was written.

Clearing attributes with clear() sounds interesting but that sounds maybe a bit out of scope.

Don't want to discourage the work you are doing because it's great to experiment like this but the more changes makes it harder get in. Can we keep the scope of this to the issue title so we can test that one change and get it in?

Then spin off follow-ups for the other ideas?

Particularly in the issue summary I'd like to keep these two items till later:

CSS class handling logic in Attribute moved to a new child class - AttributeCssClass

Id attributes handled by AttributeId. For now the class does nothing AttributeString doesn't do, but in the future it will assert that each Id sent to it is unique which should help with multiple tickets related to ID collisions.

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
8.38 KB

Ok, I'll do that. This patch only converts Attribute to an ArrayObject. I also modified the remove attributes test to work with the attributes being correctly unset. The test was calling assertEmpty but that isn't correct - Empty means the key is still set but the value is nulled out or an empty string. The original code and the comments imply that a test of whether the key was unset is needed.

The source of the problem was Attribute::removeAttribute() wasn't unsetting the keys.

joelpittet’s picture

Status: Needs review » Needs work

Nice work this looks much more managable. I think the test changes here make sense and kinda unsure how they worked before the way they were written but I would suggest maybe assertArrayNotHasKey() instead if you think that makes more sense?

  1. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -77,7 +70,11 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
    +    // Don't naively attach the attribute array, force it through the filters
    +    // set in offsetSet
    +    parent::__construct([]);
    

    Shouldn't *not* adding this do effectively the same thing or am I missing something? Also, the coding standards should be addressed here, can't remember the method call standard but I think it's ::offsetSet().? If you can clarify why this is being added, maybe the clarification needs to be added to the comment.

  2. +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
    @@ -118,23 +118,27 @@ public function testRemoveAttribute() {
    -    $this->assertEmpty($attribute['style']);
    +    $this->assertFalse(isset($attribute['style']));
    

    assertArrayNotHasKey(key, array) instead?

Aki Tendo’s picture

Status: Needs work » Needs review
FileSize
12.72 KB
7.04 KB

Ok, concerns above addressed. I forgot that assertArrayNotHasKey would work with an array object. Test coverage also expanded, see interdiff. I added the AttributeValueInterface so that I wouldn't have to turn around and rewrite my new tests in a future patch, but for the moment it's only attached to AttributeValueBase. Eventually it will replace that class.

EDIT: I changed the constructor out since I realized that wrapping must occur after exchangeArray is called as well. The unit tests make it clear why the wrapping must occur. Remember that the original constructor did a foreach loop to apply createAttributeValue as well instead of just piping the submitted array into storage.

QUESTION: Attribute will pick up the default ArrayObject implementation of the serializable interface after this patch. Should we test this in any way?

Aki Tendo’s picture

Title: Template\Attribute system optimization (uses \ArrayObject) » Refactor Drupal\Core\Template\Attribute to use ArrayObject
Priority: Major » Normal
Issue summary: View changes

Updating the issue description to entail exactly what work is being done on this ticket.

joelpittet’s picture

@Aki Tendo I don't think we have to test a feature we don't yet have a specific use-case for. That's just my opinion.

Going to run a performance test on this with xhprof-kit, feel free to try this too.
https://github.com/LionsAd/xhprof-kit

joelpittet’s picture

Status: Needs review » Needs work
FileSize
101.44 KB
45.86 KB

While setting up my performance test I noticed that with the patch the attributes were getting double escaped. We should probably resolve that first and add a regression test. See the screenshot. Cache was rebuilt between screenshots.
EDIT: ignore me, that was another patch that snuck into things.

joelpittet’s picture

Status: Needs work » Needs review

Ignore that, back to performance testing;)

Aki Tendo’s picture

Issue summary: View changes
joelpittet’s picture

Need someone else to confirm but I got some bad numbers in a performance test.

Running with PHP 5.6.27 (cli) (built: Oct 14 2016 13:57:41)

On the 8.4.x branch I got this: 2.45 seconds between 2.69 seconds
On the 2444003-104 branch I got: 12.80 seconds and 13.32 seconds

Here's the drush script:

attribute-performance-test.php

<?php

use Drupal\Core\Template\Attribute;

function providerTestAttributeData() {
  return [
    ['&"\'<>' => 'value'],
    ['title' => '&"\'<>'],
    ['class' => ['first', 'last']],
    ['disabled' => TRUE],
    ['disabled' => FALSE],
    ['alt' => ''],
    ['alt' => NULL],
    [
      'id' => 'id-test',
      'class' => ['first', 'last'],
      'alt' => 'Alternate',
    ],
    [],
  ];
}

$startTime = microtime(true);
// Your content to test
$attributesData = providerTestAttributeData();
for ($i=0; $i < 10000; $i++) {
  foreach ($attributesData as  $attributes) {
    // Convert array to attribute object.
    $attribute = new Attribute($attributes);
    // Change Attribute.
    $attribute->addClass(array('orange', 'blue'));
    $attribute->removeAttribute('id');
    // Cast to string.
    $value = (string) $attribute;
  }
}
$endTime = microtime(true);
$elapsed = $endTime - $startTime;

drush_print("Execution time : $elapsed seconds");

Run the code above in your root on each branch with the following command:
drush src attribute-performance-test.php

Aki Tendo’s picture

Issue summary: View changes

I couldn't get the script to run under kalabox so I modified it to run off my site's root using PHP directly. In that configuration the tests still favor the old code, but only by a tenth of a second. Seeing if I can get that last tenth back.

<?php

use Drupal\Core\Template\Attribute;

require_once 'autoload.php';

function providerTestAttributeData() {
  return [
    ['&"\'<>' => 'value'],
    ['title' => '&"\'<>'],
    ['class' => ['first', 'last']],
    ['disabled' => TRUE],
    ['disabled' => FALSE],
    ['alt' => ''],
    ['alt' => NULL],
    [
      'id' => 'id-test',
      'class' => ['first', 'last'],
      'alt' => 'Alternate',
    ],
    [],
  ];
}

$startTime = microtime(true);
// Your content to test
$attributesData = providerTestAttributeData();
for ($i=0; $i < 10000; $i++) {
  foreach ($attributesData as  $attributes) {
    // Convert array to attribute object.
    $attribute = new Attribute($attributes);
    // Change Attribute.
    $attribute->addClass(array('orange', 'blue'));
    $attribute->removeAttribute('id');
    // Cast to string.
    $value = (string) $attribute;
  }
}
$endTime = microtime(true);
$elapsed = $endTime - $startTime;

echo "Execution time : $elapsed seconds";

Whatever you name the script as you can invoke it with the PHP cli.

Aki Tendo’s picture

There was a notice error being raised when attempting to unset a not set element that slowed down the new code noticeably. Old code still edging it out, which doesn't make much sense since ArrayObject is implemented in C++

Aki Tendo’s picture

FileSize
690 bytes

Here's the interdiff for that.

joelpittet’s picture

Thanks @AkiTendo. I upped the loops to 100,000 from 10,000 to get some significant figures but the change and disabling xdebug helped.

With Patch:

Execution time : 10.222469091415 seconds
Execution time : 10.200246810913 seconds
Execution time : 10.391554117203 seconds

Without Patch:

Execution time : 8.6857039928436 seconds
Execution time : 8.2889668941498 seconds
Execution time : 8.6372990608215 seconds

Which is still about 20% slower, but a hell of a lot better than 520% slower!

Aki Tendo’s picture

I'm going to continue with the CSS class modification layer and see if the speed loss here can be gained back in that optimization since a large part of your test is passing through the CSS class code which is also slated for rewrite in this series of patches.

Aki Tendo’s picture

Ok, I've started over, running the speed check with each change. I'm going to move away from implementing \ArrayObject. So far I've picked up a 1/2 second improvement on this test:

<?php

use Drupal\Core\Template\Attribute;

require_once 'autoload.php';

function providerTestAttributeData() {
  return [
    ['&"\'<>' => 'value'],
    ['title' => '&"\'<>'],
    ['class' => ['first', 'last']],
    ['disabled' => TRUE],
    ['disabled' => FALSE],
    ['alt' => ''],
    ['alt' => NULL],
    [
      'id' => 'id-test',
      'class' => ['first', 'last'],
      'alt' => 'Alternate',
    ],
    [],
  ];
}

$startTime = microtime(true);
// Your content to test
$attributesData = providerTestAttributeData();
for ($i=0; $i < 100000; $i++) {
  foreach ($attributesData as  $attributes) {
    // Convert array to attribute object.
    $attribute = new Attribute($attributes);
    // Change Attribute.
    $attribute->addClass(array('orange', 'blue'));
    $attribute->removeAttribute('id');
    // Clone
    $other_attribute = new Attribute($attribute);
    // Cast to string.
    $value = (string) $attribute;
  }
}
$endTime = microtime(true);
$elapsed = $endTime - $startTime;

echo "Execution time : $elapsed seconds";

The largest change to this test is I'm also testing attribute cloning efficiency. That's testing this:

     if ($value instanceof AttributeValueBase) {
-      $class = get_class($value);
-      return new $class($name, $value->value());
+      $value = clone $value;
+      $value->setName($name);

The other change (and the one that makes an interdiff unreadable) is the createAttributeValue is removed in favor of putting it's logic directly into offsetSet, saving a function call on the stack. So far this line looks promising - 5.11 seconds on the old code, 4.5 seconds on the new. If the cloning efficiency check is skipped the speed improvement is a more narrow 3.4 for the old code, 3.2 for the new.

joelpittet’s picture

Just for your curiosity this class re-implements an ArrayObject in PHP, would be interesting to run tests between the two.

vendor/symfony/validator/Tests/Fixtures/CustomArrayObject.php

Aki Tendo’s picture

Title: Refactor Drupal\Core\Template\Attribute to use ArrayObject » Optimize Drupal\Core\Template\Attribute
Issue summary: View changes
FileSize
18.29 KB

After speed tests failed it became clear that cleanly converting the class over to /ArrayObject comes with too large a performance loss. This next patch falls back to the basic task of doing the other optimizations of the class outside that conversion, including the splitting off of CSS Class related code to it's own class.

Since I started over after the last patch failed speed trials an interdiff won't be useful.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -69,7 +69,7 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
        * @var \Drupal\Core\Template\AttributeValueBase[]
        */
    -  protected $storage = array();
    +  private $storage = [];
     
       /**
        * Constructs a \Drupal\Core\Template\Attribute object.
    @@ -77,7 +77,7 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
    
    @@ -77,7 +77,7 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
        * @param array $attributes
        *   An associative array of key-value pairs to be converted to attributes.
        */
    -  public function __construct($attributes = array()) {
    +  public function __construct($attributes = []) {
         foreach ($attributes as $name => $value) {
           $this->offsetSet($name, $value);
         }
    

    Unneeded changes. We have our own issue for those.

  2. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -87,62 +87,47 @@ public function __construct($attributes = array()) {
    +    // If the value is already an AttributeValueBase object then clone it and
    +    // inform the clone of its name in case it changed,
    

    This should contain some explantation about why this is done here.

  3. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -87,62 +87,47 @@ public function __construct($attributes = array()) {
    +      if ($name == 'class') {
    +        $value = new AttributeCssClass($value);
    +      }
    +      if (is_array($value)) {
    

    This could be an elseif here, and actually they entire branch could me merged with the outer else

  4. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -169,23 +154,13 @@ public function offsetExists($name) {
    +    if (count($args) > 0) {
    

    Why would args ever be empty?

  5. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -169,23 +154,13 @@ public function offsetExists($name) {
    +      if (!isset($this['class'])) {
    +        $this['class'] = $args;
           }
    

    Would setting $this->storage['class'] instead of $this['class'] be faster?

  6. +++ b/core/lib/Drupal/Core/Template/AttributeCssClass.php
    @@ -0,0 +1,135 @@
    +  /**
    +   * Delimiter for the implosion of the array.
    +   *
    +   * Repeated in case the parent delimiter is changed.
    +   */
    +  const DELIMITER = ' ';
    

    This could be inherited from the parent, I guess?

Aki Tendo’s picture

@dawehner
1. Ok, reverted
2. Added (see interdiff)
3. That speeds things up noticeably, so good catch.
4. Line 156 of Drupal\Tests\Core\Template\AttributeTest calls the function with no arguments and checks to see if we have no class key afterward. So, for the moment, fulfilling the test. We can probably get away with removing the test - class doesn't render when empty whether it's set or not. I don't know which of the other tests in the system might not like that change though - I'll try it if wanted.
5. It is.
6. K, done.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

markhalliwell’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
Related issues: +#2869859: [PP-1] Refactor theme hooks/registry into plugin managers

Patch no longer applies.

---

Semi-on topic. I've been working on improving this as well in https://cgit.drupalcode.org/plus/tree/src/Utility/Attributes.php?h=8.x-4.x

It'll require us to implement our own ArrayObject class, but I think it'd be better to completely re-think how we're doing attributes in general (plural, the Attribute class is misleading).

This work will likely be needed in #2869859: [PP-1] Refactor theme hooks/registry into plugin managers.

markhalliwell’s picture

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

MerryHamster’s picture

FileSize
17.04 KB

Reroll patch from #120 for 8.7.x

MerryHamster’s picture

Assigned: Aki Tendo » Unassigned
Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 126: 2444003-126.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

savkaviktor16@gmail.com’s picture

Issue tags: -Needs reroll

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.