Problem/Motivation
I am using Search API on a site with PHP 8.2. I noticed the log messages described in #3347610: PHP 8.1 preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated HtmlFilter processor, so I applied the patch from that issue. After applying the patch, I noticed that re-indexing the site is terribly slow. PHP is using 100% of CPU, and the batch process takes many minutes to process sample data (fewer than 100 nodes).
I get the same results applying the patch from #3347610 to the 8.x-1.29 release or using the 8.x-1.x-dev version.
Steps to reproduce
For local testing, I used DDev and followed the instructions at https://github.com/ddev/ddev-drupal9-solr to set up a Solr container.
- Install Drupal 10.1.5 with the Standard profile.
- Install Drush and
search_api_solrwith Composer. - Enable
search_api_solr(and its dependencies). Enable the Content datasource and the Body field (type fulltext). - Enable the HTML filter for the Body field, with default options.
- For convenience of copy/paste, disable the WYSIWYG editor for the Basic HTML text format.
- Copy https://en.wikipedia.org/wiki/Dog (attached, about 676K) into the Body field of a node. Save.
- Clear and build the index:
drush search-api:clear && time drush search-api:index.
Proposed resolution
Work-around: Disable indexing of title and alt attributes in the HTML processor. See Comments #3, #6.
Remaining tasks
| Comment | File | Size | Author |
|---|---|---|---|
| #16 | 3388678-16--fix_html_filter_performance.patch | 10.09 KB | drunken monkey |
| #16 | 3388678-16--fix_html_filter_performance--tests_only.patch | 5.85 KB | drunken monkey |
Comments
Comment #2
benjifisherAccording to XHProf, 99.9% of the CPU time comes from
mb_strpos()andmb_substr(). Both are called hundreds of thousands of times fromHtmlFilter::handleAttributes(), which is called 24 times. I think this confirms that #3347610 is responsible.My site also uses
search_api_attachments, and there are several files being indexed. It seems that those are all successfully indexed, so the problem is with some of the regular text fields, but I may be misreading it.The text is not very long:
Comment #3
drunken monkeyOh, that seems bad. Thanks a lot for reporting this!
782 kB of text does seem like a lot, but such a degradation of indexing performance is of course still not acceptable. Especially just for the sake of fixing such a small problem.
First, could you please confirm for which the “HTML filter” processor is enabled? Is it really only those that contain HTML?
Then, when removing all fulltext fields from the index except for “Body”, does the problem still occur? And does it occur with all items (with large enough body fields), or just for specific ones (maybe with faulty or particularly complex HTML)?
It seems tricky to try and optimize our current processing code, as I don’t see any obvious flaws. The best solution might be to add an option to switch between our previous and our current implementation – hardly ideal, though, I admit. (Who is going to notice that when they see that indexing takes ages?)
What you can personally do to work around this is disable indexing of the “alt” and “title” attributes. That should circumvent the whole problematic method, I think.
Comment #4
benjifisher@drunken monkey:
Thanks for the reply. I am sorry I did not reply earlier.
There is a lot going on (custom code and other) with my current site, and we may have been applying the HTML filter to non-HTML fields. So I tested the problem with a fresh install of Drupal and a single node. I just added steps to reproduce in the issue summary. I hope I did not leave anything out.
For the record:
Following the steps I provided, I get
It takes less than a second if I disable both "Index title attribute" and "Index alt attribute" in the settings for the HTML filter. With either one enabled, I get about the same (more than a minute).
If I open another terminal window and run
topin the container, then it shows that PHP is using 100% of a CPU.If I reverse the patch from #3347610: PHP 8.1 preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated HtmlFilter processor, then again it takes less than a second.
Comment #5
benjifisherIn Comment #2, I wrote,
There are over 4,000 tags in my sample file. If
mb_strpos()andmb_substr()are each called at least twice for each tag, and I have a few dozen files, that explains the "hundreds of thousands". I think my content just has too many tags for this code to handle.From #3:
I may do that for now. I do not think I will miss
titleattributes, but I would like to be able to index alt text from images.I wonder if parsing the HTML would have better performance. You can get a
\DOMDocumentobject withDrupal\Component\Utility\Html::load($text).Comment #6
martygraphie commentedHi @benjifisher,
Thank you very much for your analysis, I've encountered the same problem as you.
Deactivating ALT / TITLE attributes solved the performance problems on my side.
Marc
Comment #7
benjifisherLet' add the work-around to the issue summary.
Comment #8
chris64And what about to come back to the original in #3347610: PHP 8.1 preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated HtmlFilter processor and try an other change to solve the null parameter problem?
Comment #9
drunken monkeyThanks for the detailed information, @benjifisher, that was very helpful! So was your suggestion of using
\Drupal\Component\Utility\Html::load(), that indeed seems to be the best solution.Patch attached, even including a regression test (our first for a performance issue), please test/review!
Comment #12
balazswmann commentedThis problem affects me as well. Thank you @benjifisher very much for the investigation and root cause analysis. I run into this in the past couple of days after a
8.x-1.27=>8.x-1.30update. I also realized that indexing got very very slow. In my case it takes more than 1 minute to index just 50 items but previously (with8.x-1.27) I didn't have problems.I also use the HTML filter with the default settings and I need to index documents including around a couple of hundreds to several thousands of HTML tags (per document!).
I applied the patch from comment #9 on
8.x-1.30and it solved the problem for me. Now indexing is fast enough again. Thank you @drunken monkey!Comment #13
benjifisher@drunken monkey:
I will try applying the patch in my current project, although Comment #12 is already positive.
The code looks right to me, In fact, I often say that using DOMDocument is simpler and more reliable than "simpler" methods of processing HTML, and this is a great example of that. This is the first example I have seen where it is also more performant.
I checked the docs for
Html::load()andHtml::serialize(), and the underlyingDOMDocumentmethodsloadHTML()andsaveHTML(). None of them mention any exceptions, so I do not think you need any error handling. In fact,Html::load()explicitly suppresses warnings by calling@$dom->loadHTML(...).I am not sure what is going on with the failing test. The
foreachloop should be empty, so the code should just convert toDOMDocumentand back tostring. It is not a no-op: it should "fix" invalid HTML, and a little test shows that it strips the<character, turninga < bintoa b(two spaces). But the test results show justa. Maybe different versions of PHP, or the parsing library behind all this, account for the difference: a different version might "fix" the invalid HTML toa <b>and then "fix" that toa.I do not see any test coverage for processing both
altandtitle. I think that$xpath->query()will do the right thing and list<img src="..." alt="some text" title="other text">only once, but I would like to verify that with a test.Edit: At first, I wrote that both #8 and #12 both tested the current patch, but #8 confirmed the work-around.
Comment #14
benjifisherAs I said in the previous comment,
Html::load()followed byHtml::serialize()is not a no-op. It "normalizes" the HTML. That means the current patch will change what gets sent to be indexed. I do not think I would notice the difference, but in some use cases it might have an effect. I think we should add a change record and mention it in the release notes, so I am adding the tag for a CR.Comment #15
herved commentedI tested patch #9 on 2 projects locally and I can also see a nice performance gain.
Note that I don't have the actual files (pdf, docs, etc) locally so the impact on prod must be much more significant.
Command:
drush cr && drush sapi-c && time drush sapi-iproject 1 (using DB backend):
- without patch: 5m7,417s
- with patch: 3m31,457s
project 2 (using solr backend):
- without patch: 5m24,580s
- with patch: 1m46,645s
Also, switching to DOM parsing instead of regex makes total sense.
I'm not too worried about #14 but it's a good point.
So, +1 thanks!
Comment #16
drunken monkeyGood point, thanks! Included that in the attached patch.
I also removed the
a < btest, as that is apparently not reliable anymore. (It passed fine for me locally.)Regarding the change notice: I really think that this change is much too small to warrant a change record. Such tiny implementation details change with bug fixes all the time.
Thanks again for your continued work on this!
Comment #18
drunken monkeyCould someone confirm the latest patch worked for them and set this RTBC? Would like to get this committed and create a new release ASAP, so as few people as possible will run into this.
Comment #19
ricardopeters commented@drunken monkey I'm not sure but since 1.30 should cover D9.3 (if I read it correctly) shouldn't the code be compliant with php => 7.4. Not really sure how this should be handled with EOL php versions. We ran into an error applying the patch on a php 7.4 server hence the question. I can supply the patch with the fix, but not sure if that would be right?
Comment #20
drunken monkeyHm, yes, that’s food for thought, I guess. Both Drupal 9 and PHP 7 are now EOL, so we definitely don’t need to support them anymore. However, version 1.30 was supposed to be the last one supporting the two, so it would make sense to have a quick “patch release” with fixes for regressions support them as well. Otherwise, people still on Drupal 9 and/or PHP 7 would have no option for resolving those regressions except downgrading this module to 1.29 again.
Putting this thought into practice, though, would be rather tricky since I already merged #3394189: Drop support for Drupal 9. Therefore, the current dev version of this module is already incompatible with Drupal 9 and PHP 7. Having release 1.31 still support them would require some Git/release acrobatics that I’m not sure I want to attempt. Especially since it could mean problems for other users who do keep their installations up to date.
In conclusion, I don’t think I want to attempt to support outdated Drupal and PHP versions with release 1.31, as I’ve already gone ahead and dropped that support. If you cannot update Drupal and/or PHP for some reason (and you certainly should!), please downgrade this module to at least the commit before #3347610: PHP 8.1 preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated HtmlFilter processor.
Comment #21
ricardopeters commentedI certainly agree that it is definately not worth the effort, the downgrade to 1.29 shouldn't be that hard if you run into this and you don't have the ability to upgrade Drupal/PHP.
Should we put this somewhere in the changelog or leave it with this issue?
I ran the patch above on an 8.1 instance with the same issue and found the performance issues had resolved.
So in this case RTBC, thanks for your time and effort!
Comment #22
admirlju commentedIf the support for D9 is dropped. The module page should also be updated. At the moment it still shows support for ^9.3 || ^10.0.
When it comes to the issue, testing shows improved performance. RTBC +1.
Comment #24
drunken monkeyGood to hear, thanks for reporting back!
Merged. Thanks again!
This note is automatically generated and, more importantly, only pertains to the release under which it is printed, not to the dev version of the module. Version 1.30 still supported D9, so the note was correct. It has now vanished with the release of version 1.31.