#557542: Cache module_implements() introduced a (persistent) cache for module_implements().
It also changes the implementation of the runtime cache (which was a static variable back then, and has now changed to drupal_static_fast).
The if / else checking for the runtime cache looks like this:
if (!isset($implementations[$hook])) {
.. // discover implementations, and run hook_module_implements_alter().
}
else {
foreach ($implementations[$hook] as $module => $group) {
if ($group) {
module_load_include('inc', $module, "$module.$group");
}
if (!function_exists($module . '_' . $hook)) {
unset($implementations[$hook][$module]);
$implementations['#write_cache'] = TRUE;
}
}
}
This seems pretty stupid, because we repeatedly walk through the "else" branch for no reason, with repeated module_load_include() (if $group is not empty) and function_exists(). Duh.
The only way this could make sense is if the statically cached $implementations can change over time, from something external. This is theoretically possible, given that drupal_static() is globally accessible, but we are not that bad, are we?
PS: Don't change this to D8! This is purely a D7 issue! If D8 has a similar problem somewhere, it won't look anywhere similar to the D7 version, so this should be a separate issue.
EDIT: See #18.
The same logic is still in D8 in ModuleHandler::getImplementations() and friends. This is why the issue is now D8.
Comment | File | Size | Author |
---|---|---|---|
#69 | XHProf__Hierarchical_Profiler_Report.png | 311.95 KB | joelpittet |
#60 | D7-2263365-60-module_implements.patch | 8.38 KB | smccabe |
#56 | D7-2263365-56-module_implements.patch | 8.7 KB | smccabe |
#55 | D7-2263365-55-module_implements.patch | 9.02 KB | smccabe |
#52 | D7-2263365-52-module_implements.patch | 9.11 KB | smccabe |
Comments
Comment #1
donquixote CreditAttribution: donquixote commentedI am curious about the performance impact of this change..
Let's say a hook is called n times, and has m implementations.
n can be quite high for some hooks.
With the existing code, module_implements() costs O(n * m). After the patch, it will be only O(m).
However, module_invoke_all() will still cost O(n * m), and usually with a bigger factor than module_implements(). So it won't be magic, but still measurable.
Comment #2
donquixote CreditAttribution: donquixote commentedBtw, this is all inspired by @mikeytown2 who posted the D6 patch in #557542-179: Cache module_implements()
Comment #3
mikeytown2 CreditAttribution: mikeytown2 commentedI gave this patch a go on our D7 dev box. Without the patch I get 739ms cumulative time on 1,623 calls to module_implements. With the patch I get 83ms cumulative time on 1,623 calls to module_implements. I would call that an improvement :) Thanks for the hat tip as well. This is a RTBC from me.
Comment #4
donquixote CreditAttribution: donquixote commentedThanks!
I hope I am correctly using drupal_static() and $drupal_static_fast here. I never made friends with this stuff.
Comment #5
mikeytown2 CreditAttribution: mikeytown2 commentedIn terms of module_invoke_all() my cachegrind says that token_generate() is by far the slowest. I think the reason this is slow is due to all the calls to array_merge_recursive(). According to the docs, array_merge_recursive can take N number of arguments; could we call array_merge_recursive once at the end instead of calling it on every hook function call?
Comment #6
donquixote CreditAttribution: donquixote commented#5
I am not opposed, but should we do this in a separate issue?
Comment #7
mikeytown2 CreditAttribution: mikeytown2 commentedIt should be in a different issue. Talking about it, it sounds simple enough, but in reality the complexity needed to get it right might wash out any gains.
Comment #8
mikeytown2 CreditAttribution: mikeytown2 commentedI do have one question about the patch; why did this
change to this
Comment #9
donquixote CreditAttribution: donquixote commentedI always thought the latter was how they originally intended this to be used. But maybe I'm wrong.
It is shorter and it works, but maybe someone will perceive it as weird?
(btw, I am on IRC)
Comment #10
sunThe first variant in #8 is the common standard in Drupal core. I agree it's the most weird/useless function signature ever, but that's for another issue... Let's revert to passing $module.$group in the 3rd argument.
This patch adds some comments after / to the bottom of performed operations, which doesn't make much sense to me... those potential / expected effects should be described/documented in an inline comment that precedes the code block.
Didn't study the actual proposed change to the code logic (yet).
Comment #11
donquixote CreditAttribution: donquixote commentedChanges as suggested in #10.
Comment #12
sunIn D7 (and actually even most of D8), we're simply using "array|null"
That said, I'm fairly sure that D7 doesn't have a single instance of alternative data types in @return tags...
The common delimiter for custom/additional
drupal_static()
names throughout core is a colon (:).As this whole code is about performance, the condition should use
!isset()
instead.Comment #13
donquixote CreditAttribution: donquixote commentedAnd a lot of docblocks have no type at all.
This doesn't mean it has to stay this way.
I'd like a third opinion on this. Or is there a policy that says "D7 bronze age forever" ? :)
Good to know!
Ok.
-------------
Btw, while this is mostly motivated by performance considerations, there is one side effect to consider.
Currently, if a hook is called only once, and is not coming from the cache, then the else branch with function_exists() and module_load_include() will never happen for this hook. The if() branch already does some module_load_include() and function_exists(), but it does so before hook_module_implements_alter(). So, any additional implementations or modified $group settings won't run through module_load_include() and function_exists().
If you call module_implements() a second time, then the second branch will execute, and the modifications from hook_module_implements_alter() will be "verified". (And again every 2+nth time module_implements() is called)
This existing behavior is quite inconsistent and stupid. Either we never verify the modifications from hook_module_implements_alter(), or we do it on the first call. Doing it on every call except the first is asking for trouble.
This does generally not cause problems, because people are careful enough with hook_module_implements_alter():
- don't register nonexisting implementations
- if you change a $group, you need to include this file manually from within hook_module_implements_alter(). On subsequent requests, it will be included automatically if the hook implementations come from the cache.
As odd as this is, there could theoretically be some code out there that relies on the inconsistent behavior. E.g. if you always call module_implements() before module_invoke_all() (so module_implements() is called twice) then you are free to add any garbage you want with hook_module_implements_alter(), because it will be automatically removed on the second call to module_implements().
We have two options to clean this up:
This might cause some redundant function_exists() and module_load_include(), for stuff that was not actually modified with hook_module_implements_alter(), but this will only happen when the implementations are not coming from the cache.
The patch proposed here uses the second option.
EDIT:
The second option will make it feel easier to use hook_module_implements_alter() to change a $group.
And btw, http://drupal.stackexchange.com/questions/108884/hook-module-implements-... asking whether changing the $group is a bad practice..
Comment #14
mikeytown2 CreditAttribution: mikeytown2 commentedI'm seeing a 5~10% increase in speed in production.
Comment #15
donquixote CreditAttribution: donquixote commentedActually we should use array_diff_assoc() after hook_module_implements_alter(), and then only verify those items that changed, since the others are already verified.
Of course, for implementations from cache we should verify all items.
We should also add a test case where hook_module_implements_alter() is used to change a $group.
Comment #16
donquixote CreditAttribution: donquixote commentedBtw, about array_diff_assoc(). I am always suspicious about PHP, but this function seems to be sane. It does no unexpected magic with numeric indices, it treats duplicate values correctly, and it is consistent across PHP versions. At least if you can believe 3v4l.org.http://3v4l.org/2DAKI
Comment #17
donquixote CreditAttribution: donquixote commentedTwo patches #17a and #17b, and two interdiffs #17a vs #11 and #17b vs #17a.
#17a uses array_diff_assoc() on implementations modified with hook_module_implements_alter().
It also changes some comments and applies two of the changes suggested by @sun in #12.
#17b splits up an ugly one-liner, and moves conditionals out the foreach() loop. This is debatable change, it adds verbosity for a bit of performance which only applies in case the implementations don't come from the cache. This could be done in another way but I am going to post this version for now.
Comment #18
donquixote CreditAttribution: donquixote commentedI was wrong. D8 has the same problem, though this code has moved to a different place.
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Extension...
I don't know whether this should go in a separate issue or be dealt with here.
Comment #19
donquixote CreditAttribution: donquixote commentedBtw, ModuleHandler::getImplementations() and ModuleHandler::getImplementationInfo() are misleading names IMO, because these method have a side effect: They will include the lazy-loaded files specified with $group. Typically we don't expect such a side effect from a get*() method.
Here is a patch for D8, using git format-patch to divide into smaller steps.
Comment #20
donquixote CreditAttribution: donquixote commented#19 had a bug.
Comment #21
donquixote CreditAttribution: donquixote commentedFor D8, it is better to throw an exception than to silently remove the implementation, if hook_module_implements_alter() adds junk (undefined functions) to the array.
Also some comments fixed.
Comment #22
donquixote CreditAttribution: donquixote commentedChanged issue summary and title, to explain why this is now D8.
#19 not failing shows this is not sufficiently covered by tests.
Relevant existing tests I found are Drupal\system\Tests\Common\AlterTest::testDrupalAlter() and Drupal\system\Tests\Module\ModuleApiTest::testModuleImplements(). We probably want new test methods there.
Comment #23
donquixote CreditAttribution: donquixote commentedD8-2263365-23-module_implements-tests.patch
Introduces ModuleImplementsAlterTest, without any changes to ModuleHandler. (*)
This is expected to fail!
D8-2263365-23-module_implements.patch
Minor changes to #21 (and reroll), and merge in D8-2263365-23-module_implements-tests
D8-2263365-23-module_implements.interdiff.txt
Interdiff to #21 after rebase.
(*) About ModuleImplementsAlterTest:
I split this into a separate test class, because running ModuleApiTest locally took too much time for my taste.
We could add an additional test case to check that all implementations are "verified" when coming from the cache.
I wonder if we really need the 'standard' profile, or if the 'testing' profile is ok. I copied this from ModuleApiTest.
EDIT:
ModuleImplementsAlterTest has an unused 'use' statement, going to remove this next time.
EDIT:
Ah, crap, "Definition of Drupal\system\Tests\Module\ModuleApiTest" must be "..\ModuleImplementsAlterTest".
Comment #25
sunSee #2254203: Fix test performance of Drupal\system\Tests\Module\ModuleApiTest
Didn't look at the latest patches yet.
Comment #26
Fabianx CreditAttribution: Fabianx commentedI like #17a - I also see good speed improvements, I don't see any issues with the D8 patch, so I am gonna be bold and RTBC it for #23 - if this still passes tests.
This should go to 7.x ASAP.
Comment #29
Fabianx CreditAttribution: Fabianx commentedSadly, CNW
Comment #30
longwaveRerolled #23
Comment #31
longwaveFixed the two changes mentioned in the edits in #23
Comment #32
catchRead through this, looks like a great change, shame we missed it when we initially added the cache.
Comment #33
alexpottThe test was not firing because it was still in the PSR-0 namespace - fixed up a few other minor things.
Comment #34
Fabianx CreditAttribution: Fabianx commentedNice catch, back to RTBC.
Comment #35
Fabianx CreditAttribution: Fabianx commentedComment #36
catchCommitted/pushed to 8.0.x, thanks! Moving to 7.x for backport.
Comment #38
Fabianx CreditAttribution: Fabianx commentedCode needs review for patch in #17a.
Comment #39
Fabianx CreditAttribution: Fabianx commented**RTBM** for #17a, but unfortunately this needs test before commit so CNW.
As Tests exist in D8, might be a novice task.
Comment #41
Exploratus CreditAttribution: Exploratus commentedI implemented 17a, seems to be serving pages about 10% faster.
Comment #42
kopeboy CreditAttribution: kopeboy commentedPlease! D7 performance is important.. :(
Comment #43
toamit CreditAttribution: toamit commentedYes, please expedite commit for this patch to core
Comment #44
joelpittetI've been using this patch in #17a in production for about 4 months.
Comment #46
marcingy CreditAttribution: marcingy commentedThis needs tests
Comment #47
joseph.olstadWe're now using #17a as well. Very pleased with the improved performance.
Comment #49
johnvThe status was reset by #45, (testing the D8-patch). But #48 proves that #176 is still OK.
Comment #50
Fabianx CreditAttribution: Fabianx commentedThis unfortunately still needs tests.
Tests can be ported from D8.
Comment #51
joseph.olstadHuge thanks to smccabe for backporting the D8 tests to D7.
Comment #52
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedOk I took a shot at porting the tests over from D8, I left out some out as they seemed to be specific to D8 and I don't understand why we check for implementations.inc exactly. Hopefully I didn't butcher things too badly.
Comment #53
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedComment #54
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedThanks for the backport, still needs some work :).
should say hook_permission in the help text.
This does not make sense to me, I thought this should be included automatically?
implementation vs. implementations
--
I did not know that functionality ... Interesting ...
Comment #55
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commented1. Fixed
2. Ah yes, I looked closer and I understand this now, module load removed.
3. Fixed, I believe this typo was in the D8 patch as well.
Comment #56
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedActually just noticed one more thing, I still had some code in there for testing for an unimplemented hook throwing an exception, which isn't in D7, I've rerolled the patch with that removed.
Comment #57
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedComment #58
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentednit - Please fix those and add newlines to the end of files.
Comment #59
ChaseOnTheWebNitpick on patch #56—there are still a couple of comments referencing D8 classes.
Comment #60
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedNewlines added and D8 comments removed.
Comment #61
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedRTBC, looks great now!
Comment #62
mikeytown2 CreditAttribution: mikeytown2 commentedLooks good!
Comment #63
david_garcia CreditAttribution: david_garcia commentedWorks! RTBC++
Comment #64
joelpittetThis may seem dumb, but there is no loops removed from this patch. Only a new one added... does this need a title update?
Comment #65
joseph.olstadJoelpittet, good observation, what would be really good is a few xhprof performance profiling runs with stats before and after the patch. When in doubt put it to the test. I highly recommend xhprof, the devel module has optional xhprof integration. Set up a virtualhost for xhprof according to the installation instructions, then put that config info into devel module config. Xhprof makes a nice diagram easy to understand. This would really help us determine what type of improvements (or regressions) that we can expect. I dont have time right at this moment as I recently changed jobs and was unable to retain my dev vm environment(security paranoia) so I have a lot of setup work to do before I can be back in business. Huge thanks if you can do this xhprof for me this time, it is fun and really worth the effort.
Comment #66
joelpittet@joseph.olstad FYI xhprof was removed from devel module but it has it's own module which I've been using since it was removed. https://www.drupal.org/project/xhprof
So I've got xhprof and would love to profile this! What do you suppose a decent scenario is for testing this?
Comment #67
joseph.olstad@joelpittet , what I'd do is a very simple test plan
test 1
Prior to applying patch #60
1) drush cc all (get the xhprof profile for this)
2) hit landing page (get the xhprof profile for this)
3) hit landing page again after first time (get the xhprof profile for this)
Apply patch #60 THEN:
1) drush cc all (get the xhprof profile for this)
2) hit landing page (get the xhprof profile for this)
3) hit landing page again after first time (get the xhprof profile for this)
test 2
Repeat the same test as above, except authenticated as admin user.
see if that produces anything, repeat the test several times if necessary to get an average. Make sure this is running on hardware that you have full control over (don't run this on a Virtual Machine where you have no control over the VMHost, because this could skew the results, best if you have a controlled environment such as a LOCAL VM that is idle (no virus scans running, no scheduled tasks running, etc)
Thanks
Comment #68
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedThe 2nd loop that is repeated is no longer repeated, because the verification step is now cached as well.
As this did go in successfully for D8, I assume it should be fine for D7 as well, but I am _never_ against any profiling. :-D
Comment #69
joelpittet@joseph.olstad didn't get quite the scenario going on that but I have something...
Did drush cc all and loaded my commerce cart page. This is not a VM it's native OSX apache + brew PHP 55 with opcache enabled and xdebug is disabled.
Screenshot of the details:
Looks like a memory for time tradeoff is present but I'll do a few more scenarios and see what I get.
400ms for 4.5MB, no call differences.
Do these values look way off to any of you or what you'd expect?
Comment #70
mikeytown2 CreditAttribution: mikeytown2 commentedLooks like a good tradeoff to me.
Comment #71
donquixote CreditAttribution: donquixote as a volunteer commentedIs this really 4MB memory difference, or is the xhprof output misleading us by factor 1000?
I just copied the serialized contents of the module_implements cache in a project database to a text file, and the size of the file is 31kB. This is obviously not the same format as whatever PHP does in memory, and it is a different project. But still I would expect the PHP memory use for this function to be somehow in the same magnitude.
Comment #72
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedThat negative memory looks suspiciously like GC at the wrong point ...
Can we get the overall run instead of just the function?
Comment #73
joseph.olstadDonquixote has interesting observation, maybe beyond scope of this issue, yet deserving of an answer. I am pleased with the improvement, perhaps rtbc this and open new issue to answer donquixote above comment, this could possibly lead to further improvements in both 7.x and 8.x or to php itself.
Thanks to joelpittet for confirming performance gains!
Rtbc+1
Comment #74
joelpittet@donquixote as a comparison my module_implements cache item is 70.6 KB
@Fabianx Sorry don't seem to have those runs any longer on my machine:(
I've done a similar test with similar results the only thing is I don't have the same function calls as I did last time which is strange... but the end result was very similar. ~0.5s WT savings and 4MB mem loss.
Overall on these runs:
Comment #75
joelpittetHere's after a few runs of the same page as before was immediately after drush cc all.
Overall
Comment #76
joelpittetOne naive question, should the module_implements cache be different before and after this patch?
I seem to only have a 48 kb patch on this page after the patch (a couple of runs). The results slightly change between the patches being applied and not but not as big as I had at first after switching back and forth a few times.
Edit: getting the module_implements cache via:
If someone wants to try it out too
Comment #77
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedWell, as long as we loose 4 MB and hence have less memory used and get faster, I won't complain :).
module_implements_cache in itself would not change, but there is a new cache entry IIRC.
Comment #78
donquixote CreditAttribution: donquixote as a volunteer commentedThe 4 MB still seem weird.
One possible explanation would be if some files are included that previously were not included.
So the contents of module_implements() would be the same, but later in module_invoke_all() the function_exists() did return false, if a file was not included.
Maybe xhprof can tell us about files being included?
We would need to compare with vs without the patch, and on rebuild vs when loading from cache.
Comment #79
donquixote CreditAttribution: donquixote as a volunteer commentedBtw, another thing to compare would be the devel thingie that reports total page execution time and memory usage at the end of a page.
Comment #80
btopro CreditAttribution: btopro at Pennsylvania State University commentedbeen running this patch for awhile without issue, just adding to the RTBC crowd
Comment #81
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedAnother ++ from me, we've been running this patch on a few larger production sites for months now and have no issues. We also notice similar speed improvements to those listed in the summary.
Comment #82
donquixote CreditAttribution: donquixote as a volunteer commentedSome tests with devel memory usage report at bottom of page.
Local test site with a bunch of stuff installed.
First number is without patch, second is with patch.
Memory numbers are mostly reproducible on page refresh.
admin/content (with admin_views):
Memory used at: devel_boot()=1.22 MB, devel_shutdown()=15.21 MB, PHP peak=19.5 MB. (unpatched)
Memory used at: devel_boot()=1.22 MB, devel_shutdown()=15.22 MB, PHP peak=19.5 MB. (patched)
node/18
Memory used at: devel_boot()=1.2261 MB, devel_shutdown()=16.7769 MB, PHP peak=20.8 MB. (unpatched)
Memory used at: devel_boot()=1.2261 MB, devel_shutdown()=16.7936 MB, PHP peak=20.8 MB. (patched)
admin_menu/flush-cache (with devel redirection page)
Memory used at: devel_boot()=1.2327 MB, devel_shutdown()=43.9406 MB, PHP peak=53.75 MB. (unpatched)
Memory used at: devel_boot()=1.2286 MB, devel_shutdown()=43.9455 MB, PHP peak=53.75 MB. (unpatched)
Memory used at: devel_boot()=1.2326 MB, devel_shutdown()=43.9753 MB, PHP peak=53.75 MB. (patched)
Memory used at: devel_boot()=1.2286 MB, devel_shutdown()=43.9874 MB, PHP peak=53.75 MB. (patched)
Another site, frontpage with lots of stuff:
Memory used at: devel_boot()=2.19 MB, devel_shutdown()=39.9863 MB, PHP peak=46.25 MB. (unpatched)
Memory used at: devel_boot()=2.4359 MB, devel_shutdown()=40.0134 MB, PHP peak=46 MB. (unpatched)
Memory used at: devel_boot()=2.1909 MB, devel_shutdown()=39.988 MB, PHP peak=46.25 MB. (unpatched)
Memory used at: devel_boot()=2.1909 MB, devel_shutdown()=40.0088 MB, PHP peak=46 MB. (unpatched)
Memory used at: devel_boot()=2.3212 MB, devel_shutdown()=40.1541 MB, PHP peak=46.5 MB. (patched)
Memory used at: devel_boot()=2.3212 MB, devel_shutdown()=40.1693 MB, PHP peak=45.75 MB. (patched)
Memory used at: devel_boot()=2.3212 MB, devel_shutdown()=40.1699 MB, PHP peak=45.75 MB. (patched)
Another site, commerce shopping cart page:
Memory used at: devel_boot()=5.1632 MB, devel_shutdown()=54.323 MB, PHP peak=54.75 MB. (unpatched)
Memory used at: devel_boot()=5.1632 MB, devel_shutdown()=54.323 MB, PHP peak=54.75 MB. (unpatched)
Memory used at: devel_boot()=5.1671 MB, devel_shutdown()=54.3526 MB, PHP peak=54.75 MB. (patched)
Memory used at: devel_boot()=5.1671 MB, devel_shutdown()=54.3532 MB, PHP peak=54.75 MB. (patched)
Also did some xhprof memory profiling, to specifically look at the memory footprint of module_implements().
I did not find a significant increase in memory usage. Any difference I found was far away from the 4MB discussed above.
@joelpittet (#69): It seems your memory issues are mostly coming from drupal_alter(). Maybe there is an alter hook that is particularly nasty? You can find it by navigating up from module_implements() along the most expensive parent functions in the xhprof output.
Comment #83
joseph.olstadThis patch works well against the latest drupal 7.39
still rtbc
Comment #84
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedPatch looks great and seems to have been very well-tested. Committed to 7.x - thanks!
I fixed a few small things on commit (the first two just because the $reset parameter to module_implements() is for internal use only, so I thought having the PHPDoc reference the case where it's TRUE too much would be confusing for most developers trying to use this function):
Comment #86
donquixote CreditAttribution: donquixote as a volunteer commentedThank you so much!