#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.

CommentFileSizeAuthor
#69 XHProf__Hierarchical_Profiler_Report.png311.95 KBjoelpittet
#60 D7-2263365-60-module_implements.patch8.38 KBsmccabe
#56 D7-2263365-56-module_implements.patch8.7 KBsmccabe
#55 D7-2263365-55-module_implements.patch9.02 KBsmccabe
#52 D7-2263365-52-module_implements.patch9.11 KBsmccabe
#33 2263365.33.patch14.07 KBalexpott
#33 31-33-interdiff.txt5.77 KBalexpott
#31 D8-2263365-31-module_implements.interdiff.txt681 byteslongwave
#31 D8-2263365-31-module_implements.patch14.46 KBlongwave
#30 D8-2263365-30-module_implements.patch14.49 KBlongwave
#1 D7-2263365-1-module_implements.patch3.73 KBdonquixote
#11 D7-2263365-11-module_implements.interdiff.txt1.67 KBdonquixote
#11 D7-2263365-11-module_implements.patch3.36 KBdonquixote
#17 D7-2263365-17a-module_implements.interdiff.txt3.92 KBdonquixote
#17 D7-2263365-17a-module_implements.patch5.09 KBdonquixote
#17 D7-2263365-17b-module_implements.interdiff.txt2.4 KBdonquixote
#17 D7-2263365-17b-module_implements.patch6.57 KBdonquixote
#19 D8-2263365-19-module_implements.patch10.91 KBdonquixote
#20 D8-2263365-20-module_implements.interdiff.txt778 bytesdonquixote
#20 D8-2263365-20-module_implements.patch10.92 KBdonquixote
#21 D8-2263365-21-module_implements.interdiff.txt1.62 KBdonquixote
#21 D8-2263365-21-module_implements.patch10.88 KBdonquixote
#23 D8-2263365-23-module_implements.interdiff.txt8.42 KBdonquixote
#23 D8-2263365-23-module_implements.patch21.49 KBdonquixote
#23 D8-2263365-23-module_implements-tests.patch6.85 KBdonquixote
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

donquixote’s picture

Status: Active » Needs review
FileSize
3.73 KB

I 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.

donquixote’s picture

Btw, this is all inspired by @mikeytown2 who posted the D6 patch in #557542-179: Cache module_implements()

mikeytown2’s picture

I 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.

donquixote’s picture

Thanks!
I hope I am correctly using drupal_static() and $drupal_static_fast here. I never made friends with this stuff.

mikeytown2’s picture

In 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?

donquixote’s picture

#5
I am not opposed, but should we do this in a separate issue?

mikeytown2’s picture

It 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.

mikeytown2’s picture

I do have one question about the patch; why did this

module_load_include('inc', $module, "$module.$group");

change to this

module_load_include($group . '.inc', $module);
donquixote’s picture

I 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)

sun’s picture

Component: base system » extension system
Issue tags: -module_implements

The 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).

donquixote’s picture

Changes as suggested in #10.

sun’s picture

  1. + * @return string[]|null
    

    In 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...

  2. +    $drupal_static_fast['verified'] = &drupal_static(__FUNCTION__ . '.verified');
    

    The common delimiter for custom/additional drupal_static() names throughout core is a colon (:).

  3. +  if (empty($verified[$hook])) {
    

    As this whole code is about performance, the condition should use !isset() instead.

donquixote’s picture

In D7 (and actually even most of D8), we're simply using "array|null"

And 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" ? :)

The common delimiter for custom/additional drupal_static() names throughout core is a colon (:).

Good to know!

As this whole code is about performance, the condition should use !isset() instead.

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:

  1. Only "verify" (once per request and per hook) implementations that come from the cache. Don't verify implementations modified with hook_module_implements_alter(). It will be the job of whoever implements this hook to make sure the function exists and the file is included. We can assume this is already the case for most implementations of hook_module_implements_alter(), but there might be some really weird edge cases as mentioned above, where this could break existing code.
  2. "Verify" (once per request and per hook) implementations coming from the cache, and those possibly modified with hook_module_implements_alter(). This means, people will now be free to add undefined functions in hook_module_implements_alter(), and modify the $group without manually including the file.
    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..

mikeytown2’s picture

I'm seeing a 5~10% increase in speed in production.

donquixote’s picture

Actually 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.

donquixote’s picture

Btw, 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

donquixote’s picture

Two 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.

donquixote’s picture

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.

I 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.

donquixote’s picture

Version: 7.x-dev » 8.x-dev
FileSize
10.91 KB

Btw, 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.

donquixote’s picture

#19 had a bug.

donquixote’s picture

For 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.

donquixote’s picture

Title: D7: Second loop in module_implements() being repeated for no reason. » D7+D8: Second loop in module_implements() being repeated for no reason.
Issue summary: View changes

Changed 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.

donquixote’s picture

D8-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".

The last submitted patch, 23: D8-2263365-23-module_implements-tests.patch, failed testing.

sun’s picture

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

I 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.

The last submitted patch, 23: D8-2263365-23-module_implements.patch, failed testing.

Fabianx’s picture

Status: Reviewed & tested by the community » Needs work

Sadly, CNW

longwave’s picture

Status: Needs work » Needs review
FileSize
14.49 KB

Rerolled #23

longwave’s picture

Fixed the two changes mentioned in the edits in #23

catch’s picture

Status: Needs review » Reviewed & tested by the community

Read through this, looks like a great change, shame we missed it when we initially added the cache.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
5.77 KB
14.07 KB

The test was not firing because it was still in the PSR-0 namespace - fixed up a few other minor things.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Nice catch, back to RTBC.

Fabianx’s picture

Issue tags: +Needs backport to D7
catch’s picture

Title: D7+D8: Second loop in module_implements() being repeated for no reason. » Second loop in module_implements() being repeated for no reason.
Version: 8.0.x-dev » 7.x-dev
Status: Reviewed & tested by the community » Needs review

Committed/pushed to 8.0.x, thanks! Moving to 7.x for backport.

  • catch committed b0eda32 on 8.0.x
    Issue #2263365 by donquixote, longwave, alexpott: Second loop in...
Fabianx’s picture

Code needs review for patch in #17a.

Fabianx’s picture

Priority: Normal » Major
Status: Needs review » Needs work
Issue tags: +Needs tests, +Novice

**RTBM** for #17a, but unfortunately this needs test before commit so CNW.

As Tests exist in D8, might be a novice task.

Exploratus’s picture

I implemented 17a, seems to be serving pages about 10% faster.

kopeboy’s picture

Please! D7 performance is important.. :(

toamit’s picture

Yes, please expedite commit for this patch to core

joelpittet’s picture

Status: Needs work » Reviewed & tested by the community

I've been using this patch in #17a in production for about 4 months.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 33: 2263365.33.patch, failed testing.

marcingy’s picture

This needs tests

joseph.olstad’s picture

We're now using #17a as well. Very pleased with the improved performance.

johnv’s picture

Status: Needs work » Reviewed & tested by the community

The status was reset by #45, (testing the D8-patch). But #48 proves that #176 is still OK.

Fabianx’s picture

Status: Reviewed & tested by the community » Needs work

This unfortunately still needs tests.

Tests can be ported from D8.

joseph.olstad’s picture

Huge thanks to smccabe for backporting the D8 tests to D7.

smccabe’s picture

Ok 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.

smccabe’s picture

Status: Needs work » Needs review
Fabianx’s picture

Status: Needs review » Needs work

Thanks for the backport, still needs some work :).

  1. +++ b/modules/simpletest/tests/module.test
    @@ -302,3 +302,49 @@ class ModuleUninstallTestCase extends DrupalWebTestCase {
    +    $modules = module_implements('permission');
    +    $this->assertTrue(in_array('module_test', $modules), 'module_test implements hook_module_implements_alter().');
    

    should say hook_permission in the help text.

  2. +++ b/modules/simpletest/tests/module.test
    @@ -302,3 +302,49 @@ class ModuleUninstallTestCase extends DrupalWebTestCase {
    +    // Assert that module_test_module_implements_alter(*, 'altered_test_hook')
    +    // has added an implementation
    +    module_load_include('inc', 'module_test', 'module_test.implementations');
    ...
    +    // Assert that module_test.implementations.inc was included as part of the process.
    +    $this->assertTrue(function_exists('module_test_altered_test_hook'),
    +      'The file module_test.implementations.inc was included.');
    

    This does not make sense to me, I thought this should be included automatically?

  3. +++ b/modules/simpletest/tests/module_test.module
    @@ -129,3 +129,23 @@ function module_test_modules_uninstalled($modules) {
    +    // Add a hook implementation, that will be found in
    +    // module_test.implementation.inc.
    +    $implementations['module_test'] = 'implementations';
    

    implementation vs. implementations

    --

    I did not know that functionality ... Interesting ...

smccabe’s picture

1. 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.

smccabe’s picture

Actually 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.

smccabe’s picture

Status: Needs work » Needs review
Fabianx’s picture

+++ b/modules/simpletest/tests/module_test.implementations.inc
@@ -0,0 +1,10 @@
\ No newline at end of file

+++ b/modules/simpletest/tests/module_test.module
@@ -129,3 +129,17 @@ function module_test_modules_uninstalled($modules) {
\ No newline at end of file

nit - Please fix those and add newlines to the end of files.

ChaseOnTheWeb’s picture

Nitpick on patch #56—there are still a couple of comments referencing D8 classes.

+   * @see \Drupal\Core\Extension\ModuleHandler::buildImplementationInfo()
+ * @see \Drupal\system\Tests\Module\ModuleImplementsAlterTest::testModuleImplementsAlter()
smccabe’s picture

Newlines added and D8 comments removed.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, looks great now!

mikeytown2’s picture

Looks good!

david_garcia’s picture

Works! RTBC++

joelpittet’s picture

This may seem dumb, but there is no loops removed from this patch. Only a new one added... does this need a title update?

joseph.olstad’s picture

Joelpittet, 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.

joelpittet’s picture

Issue tags: +needs profiling

@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?

joseph.olstad’s picture

@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

Fabianx’s picture

The 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

joelpittet’s picture

Issue summary: View changes
FileSize
311.95 KB

@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.

module_implements Run #5583ad7de52c3 Run #5583aafbef44a Diff Diff%
Number of Function Calls 4,527 4,527 0 0.0%
Incl. Wall Time (microsec) 3,367,667 2,903,452 -464,215 -13.8%
Incl. Wall Time (microsec) per call 744 641 -103 -13.8%
Excl. Wall Time (microsec) 1,168,962 844,643 -324,319 -27.7%
Incl. CPU (microsecs) 2,925,278 2,593,691 -331,587 -11.3%
Incl. CPU (microsecs) per call 646 573 -73 -11.3%
Excl. CPU (microsec) 876,695 635,810 -240,885 -27.5%
Incl. MemUse (bytes) -1,273,984 3,279,496 4,553,480 357.4%
Incl. MemUse (bytes) per call -281 724 1,006 357.4%
Excl. MemUse (bytes) -38,358,408 -33,826,192 4,532,216 11.8%
Incl. PeakMemUse (bytes) 3,247,000 3,417,968 170,968 5.3%
Incl. PeakMemUse (bytes) per call 717 755 38 5.3%
Excl. PeakMemUse (bytes) 56,024 114,128 58,104 103.7%

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?

mikeytown2’s picture

Looks like a good tradeoff to me.

donquixote’s picture

Is 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.

Fabianx’s picture

That negative memory looks suspiciously like GC at the wrong point ...

Can we get the overall run instead of just the function?

joseph.olstad’s picture

Donquixote 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

joelpittet’s picture

@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.

module_implements Run #5587a3d29e3b5 Run #5587a246ad72f Diff Diff%
Number of Function Calls 4,642 3,331 -1,311 -28.2%
Incl. Wall Time (microsec) 2,132,978 1,587,312 -545,666 -25.6%
Incl. Wall Time (microsec) per call 459 477 17 3.7%
Excl. Wall Time (microsec) 690,540 495,101 -195,439 -28.3%
Incl. CPU (microsecs) 1,987,715 1,514,635 -473,080 -23.8%
Incl. CPU (microsecs) per call 428 455 27 6.2%
Excl. CPU (microsec) 476,076 360,382 -115,694 -24.3%
Incl. MemUse (bytes) 3,244,648 2,418,672 -825,976 -25.5%
Incl. MemUse (bytes) per call 699 726 27 3.9%
Excl. MemUse (bytes) -34,071,080 -29,413,744 4,657,336 13.7%
Incl. PeakMemUse (bytes) 3,739,984 1,586,576 -2,153,408 -57.6%
Incl. PeakMemUse (bytes) per call 806 476 -329 -40.9%
Excl. PeakMemUse (bytes) 74,824 62,776 -12,048 -16.1%

Overall on these runs:

Run #5587a3d29e3b5 Run #5587a246ad72f Diff Diff%
Number of Function Calls 3,055,050 2,462,020 -593,030 -19.4%
Incl. Wall Time (microsec) 25,353,101 19,509,769 -5,843,332 -23.0%
Incl. CPU (microsecs) 19,895,577 15,284,174 -4,611,403 -23.2%
Incl. MemUse (bytes) 124,832,016 124,316,736 -515,280 -0.4%
Incl. PeakMemUse (bytes) 134,210,512 128,086,472 -6,124,040 -4.6%
joelpittet’s picture

Issue tags: -needs profiling

Here's after a few runs of the same page as before was immediately after drush cc all.

module_implements Run #5587a595b8780 Run #5587a666e2a0a Diff Diff%
Number of Function Calls 1,403 1,403 0 0.0%
Incl. Wall Time (microsec) 26,354 14,508 -11,846 -44.9%
Incl. Wall Time (microsec) per call 19 10 -8 -44.9%
Excl. Wall Time (microsec) 14,629 9,933 -4,696 -32.1%
Incl. CPU (microsecs) 26,989 15,474 -11,515 -42.7%
Incl. CPU (microsecs) per call 19 11 -8 -42.7%
Excl. CPU (microsec) 12,467 9,422 -3,045 -24.4%
Incl. MemUse (bytes) 1,152,944 944,240 -208,704 -18.1%
Incl. MemUse (bytes) per call 822 673 -149 -18.1%
Excl. MemUse (bytes) 100,152 141,192 41,040 41.0%
Incl. PeakMemUse (bytes) 479,208 175,096 -304,112 -63.5%
Incl. PeakMemUse (bytes) per call 342 125 -217 -63.5%
Excl. PeakMemUse (bytes) 12,856 13,232 376 2.9%

Overall

Run #5587a595b8780 Run #5587a666e2a0a Diff Diff%
Number of Function Calls 546,038 543,323 -2,715 -0.5%
Incl. Wall Time (microsec) 3,356,500 3,698,294 341,794 10.2%
Incl. CPU (microsecs) 2,995,301 3,268,233 272,932 9.1%
Incl. MemUse (bytes) 67,703,248 67,384,392 -318,856 -0.5%
Incl. PeakMemUse (bytes) 67,961,560 67,648,328 -313,232 -0.5%
joelpittet’s picture

One 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:

drush cg module_implements cache_bootstrap > module_implements-before.txt
drush cg module_implements cache_bootstrap > module_implements-after.txt

If someone wants to try it out too

Fabianx’s picture

Well, 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.

donquixote’s picture

The 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.

donquixote’s picture

Btw, another thing to compare would be the devel thingie that reports total page execution time and memory usage at the end of a page.

btopro’s picture

been running this patch for awhile without issue, just adding to the RTBC crowd

smccabe’s picture

Another ++ 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.

donquixote’s picture

Some 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.

joseph.olstad’s picture

This patch works well against the latest drupal 7.39
still rtbc

David_Rothstein’s picture

Status: Reviewed & tested by the community » Fixed

Patch 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):

diff --git a/includes/module.inc b/includes/module.inc
index f4a752d..076992c 100644
--- a/includes/module.inc
+++ b/includes/module.inc
@@ -677,8 +677,8 @@ function module_hook($module, $hook) {
  * Determines which modules are implementing a hook.
  *
  * Lazy-loaded include files specified with "group" via hook_hook_info() or
- * hook_module_implements_alter() will be automatically included as part of
- * module_implements(*, *, FALSE).
+ * hook_module_implements_alter() will be automatically included by this
+ * function when necessary.
  *
  * @param string $hook
  *   The name of the hook (e.g. "help" or "menu").
@@ -690,7 +690,7 @@ function module_hook($module, $hook) {
  *   implementations to be regenerated (such as after enabling a new module,
  *   before processing hook_enable).
  *
- * @return string[]|null
+ * @return
  *   An array with the names of the modules which are implementing this hook.
  *
  * @see module_implements_write_cache()
@@ -722,7 +722,7 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
     drupal_static_reset('module_hook_info');
     drupal_static_reset('drupal_alter');
     cache_clear_all('hook_info', 'cache_bootstrap');
-    return NULL;
+    return;
   }
 
   // Fetch implementations from cache.
@@ -778,7 +778,7 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
         }
       }
     }
-    // Implementations for this hook are now "verified"
+    // Implementations for this hook are now "verified".
     $verified[$hook] = TRUE;
   }
   elseif (!isset($verified[$hook])) {
diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test
index 73f6327..eea3b51 100644
--- a/modules/simpletest/tests/module.test
+++ b/modules/simpletest/tests/module.test
@@ -307,7 +307,7 @@ class ModuleImplementsAlterTestCase extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
       'name' => 'Module implements alter',
-      'description' => 'Tests the hook_module_implements_alter.',
+      'description' => 'Tests hook_module_implements_alter().',
       'group' => 'Module',
     );
   }

  • David_Rothstein committed 3c8b182 on 7.x
    Issue #2263365 by donquixote, smccabe, longwave, alexpott, joelpittet,...
donquixote’s picture

Thank you so much!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.