This issue/patch advances on #2185559: Extension System, Part I: SystemListing clean-up + performance; remove SystemListingInfo + drupal_system_listing() — we either skip that issue and directly go with this, or this patch can be easily rebased against 8.x with that patch.
Part I + II of #2186491: [meta] D8 Extension System: Discovery/Listing/Info
Problem
SystemListingInfo::scan()
triggers an infinite recursion to itself when scanning for installation profiles, because it tries to retrieve the profile directories to be scanned, which makes no sense.- The installer scans for available installation profiles, but does not prime the static filepath lookup cache of
drupal_get_filename()
, which causes the first of call todrupal_get_path()
from other code to trigger yet another filesystem scan. - The results of
SystemListing
are not cached, even though a subsequent scan will yield the identical results. - The results of
InfoParser
are not cached, even though a subsequent info file parsing will yield the identical results. hook_system_theme_info()
only exists to allow modules to ship with test themes, whereas discovering themes is and should be the sole responsibility of the extension discovery process.
Proposed solution
-
Replace
SystemListing[Info]
with a highly performance-optimizedExtensionDiscovery
:Bench: 10 scans for 'modules' SystemListing (HEAD): 6.825 seconds ExtensionDiscovery: 0.391 seconds
- Require
.info.yml
files for all extension types. - Instead of scanning a particular base/origin directory (e.g.,
/sites/all
) for the requested extension type only, scan it once for all extensions (of all types). - Use a highly optimized filter for the recursive filesystem scan, 100% tailored to the specific task of discovering Drupal extensions.
- Do not recurse into
'tests'
directories at regular runtime (~400% performance increase on its own). - Return proper
Extension
objects for all discovered extensions.
→ Substantial extension discovery performance increase. Installer responds like a breeze.
- Require
- Fix the installer to prime
drupal_get_filename()
after scanning for installation profiles to resolve the infinite recursion problem. - Statically cache all discovered extensions.
- Statically cache the
InfoParser
results to prevent the same info files from being re-parsed again. - Replace all instances of
drupal_system_listing()
withExtensionDiscovery
.
API changes
-
All Drupal extension types (profiles, modules, themes, and also theme engines) have to supply an
.info.yml
file in order to be discovered.- The info file must specify at least the
'type'
,'core'
, and'name'
properties. - The
'type'
value for theme engines istheme_engine
. - The
'type'
property should ideally be declared first (for file parsing performance reasons only).
Example:
type: theme_engine name: Twig core: 8.x version: VERSION package: Core
- The info file must specify at least the
-
drupal_system_listing()
has been replaced withDrupal\Core\Extension\ExtensionDiscovery
.Drupal 7
$available_modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
Drupal 8
$listing = new ExtensionDiscovery(); $available_modules = $listing->scan('module'); // Note that the returned entries are Extension objects, offering access to // SplFileInfo of the .info.yml file. // @see http://php.net/manual/class.splfileinfo.php // For example: $filemtime = $available_modules['node']->getMTime();
-
hook_system_theme_info() has been removed. Additional (test) themes provided by modules are discovered natively now.
-
(8.x-only)
SystemListing
andSystemListingInfo
have been removed.
Follow-up issues
- Remove
/sites/all
, duplicates top-level directories. - Consider to remove core compatibility check in
ExtensionDiscovery
, obsolete with Migrate in core? - Convert Database drivers into a new + regular extension type?
- Move
'type'
properties in.info.yml
files to be defined first for performance (lead by example).
Comment | File | Size | Author |
---|---|---|---|
#68 | extension.disco_.68.patch | 114.58 KB | andypost |
#66 | interdiff.txt | 1.05 KB | sun |
Comments
Comment #1
sunComment #3
sunComment #5
sunFixed drupal_get_filename() is called in special test environment conditions.
Comment #7
sunComment #9
sunRebased against HEAD.
Comment #11
sunFixed UpdateManager::projectStorage() does not ensure that project list is an array (k/v can return NULL).
Comment #12
sunGreat, we're green! :)
Comment #13
sunUpdated ThemeHandlerTest for the parameter rename accordingly.
Will cancel the previous test in a moment.
Comment #15
sunThis pretty much removes and/or resolves all remaining @todos that are within the scope of this issue.
All remaining @todos in this patch are only documenting and clarifying status quo of HEAD and will be addressed in separate child issues of the Extension System clean-up/conversion meta issue.
In other words, I think this patch is ready and resolves the first step of an ExtensionDiscovery in a very solid + performant way.
Comment #17
sunHm. I'm not able to reproduce those test failures locally, so I assume that was a testbot fluke for now.
Anyway, some further final polishing while waiting for reviews:
Comment #18
sunWhile waiting for reviews, further polishing some minor aspects…
Comment #19
larowlanCode review, manual testing to come:
I think policy dictates we will need to leave this around, as a wrapper and mark deprecated. But you'll need to ask a committer.
Out of scope, please open a new issue
good cleanup, but out of scope - separate issue?
can you wrap this in an extensionDirectory() method like you have for the theme handler, so we can mock for phpunit
should we add .git here? I would regularly have a contrib module in it's own checkout, no need to traverse into the .git folder
nevermind :)
> 80 chars
Should this have an interface and be a service (so can be injected etc), I realise its low-level, but there are high-level uses of drupal_system_listing() in contrib in D7
Default mismatch, one says NULL, the other FALSE
Should there be an ORIGIN_PROFILE here? if so we must be missing tests. If not, then I've missed something.
fetching it all in once pass++
Parent profiles? Do we support that?
Out of scope?
mismatch $profileDirectories Vs $paths
out of scope?
Yay for less arbitrary arrays, more domain objects
note for other reviewers: this function is gone
again, makes unit testing difficult, can we use a method like for ThemeHandler, given it extends InstallStorage, should be do-able
Does anything use this functionality (using drupal_get_filename with random arguments), surely it was added for a reason (and with tests). Worth grepping the git logs?
Nice!
Out of scope?
Comment #20
larowlanManual testing as follows:
so my comments about ORIGIN_PROFILE don't apply here
Comment #21
sunThanks for your detailed review, @larowlan! :-)
Not going to address each of your points individually (most of them are very minor anyway), so on the main points:
drupal_system_listing()
is removed here, because it allows to scan the full filesystem for arbitrary files, but only within extension directories, for which it cannot use ExtensionDiscovery anymore, because that is fully tailored and performance-optimized to specifically find extensions only.Modules should not actually perform raw filesystem scans in the first place. The public API idea of
drupal_system_listing()
was that you can find files of a certain pattern within all extensions, regardless of whether installed or not.This public API use-case should be replaced with a new extension list based file discovery facility on the upcoming ExtensionList/ExtensionHandler service, which limits the lookup to installed extensions.
All of that being said, the edge-case of performing a raw filesystem scan across all available extensions is still possible and as easy as this:
Given how simple this is, and given how much of an edge-case the use-case is in the first place, I actually think we should not provide any kind of helper function for it.
The much more common use-case is the following anyway, based on installed modules (instead of all available):
And that's essentially the extension list based discovery mechanism that could/should exist on the upcoming ExtensionList/ExtensionHandler, because this exact concept is required pretty much all across Drupal core (cf. routing, theme templates, libraries, etc.pp.).
I want to leave the minor changes to the database driver installer code in this patch, because database drivers apparently are an extension type that I and everyone else continues to forget about. → They are not even part of the meta issue summary, because I also forgot about them.
Case in point: This new
ExtensionDiscovery
allows us to convert custom database drivers into regular extension types. This cuts it:→ scans all
./drivers
directories in all origins for database driver extensions, and in turn:→ The minor cleanups and added @todo is here to raise awareness of the full potential and impact of this patch.
The
ExtensionDiscovery
class should not be exposed as a service, because only a few subsystems in core should need and use it. In addition, theExtensionDiscovery
is a dependency of the serviceContainer
build process itself.All other subsystems, services, and modules should never have to discover extensions. They solely operate on the list of installed extensions only.
Replacing the direct
new ExtensionDiscovery()
with class methods is out of scope here, since that only makes sense when phpunits for these classes are actually introduced."Parent installation profile" is a concept that belongs to Simpletest (web) tests, as also outlined in the meta issue.
Comment #23
sunUpdated for #2171683: Remove all Simpletest overrides and rely on native multi-site functionality instead ;-)
Comment #24
sun23: extension.disco_.23.patch queued for re-testing.
Comment #26
sunMerged HEAD.
Comment #27
damiankloip CreditAttribution: damiankloip commentedOverall, I like this patch alot.
Do we use (optional) An array.. instead?
This could be a much broader question/issue really. However, might as well bring it up here. Do you think this should return NULL and not FALSE. So we are not mixing bool/strings? (minor issue)
Pass a bool here are the default parameter value maybe.
Might make sense to store the value of $this->current() as a variable instead in this method?
Could use \Drupal::cache() directly but don't really mind. Would be pretty nice to remove cache.inc soon though. There isn't much in there nowadays.
Comment #28
sunUsage of
$this->current()
is a common/standard coding practice forFilterIterator
implementations, as far as I can tell from code I studied across the net.I do not plan to address #27.1 and .5 here.
Comment #29
ryan.armstrong CreditAttribution: ryan.armstrong commentedThis is looking awesome Sun. I have run into multiple issues with discovery of extensions, so to see this rewrite is great!
I tested the patch from 28. For each module, profile and theme that I created, I only added a .info.yml file as that should be the only requirement to detect the module/theme. Also, when I note a success I mean that the module was shown on the admin/extend page, the theme was shown on the admin/appearance page, or the profile was shown as an option when running install.php. I'm assuming that issues regarding actually enabling these modules, themes, and profiles, which only contain info.yml files is another issue.
Modules:
Themes:
Profiles:
Thoughts
Is there anything else that needs to be tested? Otherwise I would consider this RTBC depending on the response to my questions. Again, awesome work Sun!
Comment #30
sunThanks for reviewing and testing, @ryan.armstrong! :-)
I agree that this is in RTBC quality. Regarding your additional thoughts on the test results:
Yeah, the Themes/Appearance page uses an additional database cache layer, so the extension discovery is not actually invoked when you visit that page.
That behavior is not changed here, but may be fixed by #1067408: Themes do not have an installation status — although the additional caching behavior always existed to my knowledge; I'm not sure why we made a difference between the modules page and the themes/appearance page...
Yeah, that is sadly still expected behavior. This patch is the blocker for #340723: Make modules and installation profiles only require .info.yml files — only with that additional change, you will be able to install a profile/module by just creating the .info.yml file.
Strictly speaking, you actually discovered a bug by saying that, because the modules are exposed in the UI, even though they don't have a .module file. ;) But I don't think it makes sense to "fix" that, because once this patch is in, #340723: Make modules and installation profiles only require .info.yml files will be piece of cake. :-)
As of now, the 'type' can be declared anywhere in an .info.yml file. The discovery performance is best if the 'type' is declared first, since every .info.yml file is parsed for its 'type'. — To avoid the overhead of YAML parsers, the 'type' property is manually parsed/extracted from the .infoyml file, whereas the file is read line by line until the 'type' declaration is found. Hence: If declared first, exactly one line is read only.
I don't think we need to enforce it to be declared first, but as a follow-up issue, I think Drupal core should simply lead by example and have it declared first in all .info.yml files in core. That is, because most people are simply following what core is doing.
Comment #31
ryan.armstrong CreditAttribution: ryan.armstrong commentedAwesome, thanks for the detailed response. I'm marking this as RTBC, let's get this committed! In addition to leading by example with core's modules, we should also make sure that the change record as well as the documentation also note this, as this is something that could easily be lost in the shuffle.
Comment #32
damiankloip CreditAttribution: damiankloip commented27.1 - fair enough. I don't really care about that
27.5 - I was not meaning remove cache.inc in this issue. Here you are adding calls to cache() though, which is not preferable to \Drupal::cache().
This can be removed in another issues though, I don't mind either way. If you're really against changing it for whatever reason.
Comment #33
damiankloip CreditAttribution: damiankloip commentedOK, just for reference, created #2198339: Remove cache.inc to remove cache.inc (Also see child issue that is postponed on).
Comment #34
sunCreated the change notice: https://drupal.org/node/2198695
Comment #35
sunCreated a separate follow-up issue for one (rather minor) concern that came up in the previous/original issue #2185559: Extension System, Part I: SystemListing clean-up + performance; remove SystemListingInfo + drupal_system_listing():
#2198713: Add a settings.php variable to allow to discover/enable test extensions at regular runtime
Comment #36
catchIs this a requirement or optional?
Comment #37
ryan.armstrong CreditAttribution: ryan.armstrong commentedAs of now its optional. You can actually put the type anywhere in the info.yml but you get a performance bump if you put it first.
Comment #38
alexpottextension.disco_.28.patch no longer applies.
Comment #39
sunSimple diff context conflict. 8.x merged cleanly.
Comment #40
webchickThis seems like it'd be good for catch to review.
Comment #41
damiankloip CreditAttribution: damiankloip commentedThe usages of the cache() function still need to be changed. This is now deprecated and would reintroduce it, meaning the only two usages in core again.
Not sure why you refused to do this in my previous feedback tbh ;)
Comment #42
damiankloip CreditAttribution: damiankloip commentedComment #43
BerdirFixed those calls. No commit credit, this is all sun's doing :)
Comment #44
damiankloip CreditAttribution: damiankloip commentedThanks!
Comment #45
alexpottextension.disco_.43.patch no longer applies.
Comment #46
BerdirTrivial re-roll, only conflicted on a new use statement.
Comment #47
alexpottextension.disco_.46.patch no longer applies.
Comment #48
catchOverall looks great, few things I noticed:
Something in me was hoping this function would completely die. It's great that it's a lot shorter, is there a part ? where this gets moved to the extension system proper? (Fine if there isn't for this patch, just wondering).
Not sure about this, we don't do the same for lots of other services and seems like this would only ever be used in the installer. Could we remove the @todo and add a follow-up issue instead? The discovery here is no worse than it already is, so doesn't need a @todo (which is more of an apology than an intention to do anything usually).
purdy.
No need for 'an'.
What if there's a module with the same name as one of these directories?
Does this need to be specific to tests, or could we add a setting, then override that setting when running tests? Just thinking whether there's a use case for extending the blacklisted directories during normal operation.
No need to cast to bool.
Should we open a follow-up issue to rename 'config' module to 'configuration? I understand the optimization here but it's a bit nasty.
Really? Could at least do with a @todo, but I'm having trouble seeing why this is all necessary.
Not really replaced.
Might be worth putting in a dedicated method, we must be doing the same check elsewhere.
Not a backup, just 'store' or 'retain'?
Nice.
Comment #49
YesCT CreditAttribution: YesCT commentedI'm going to reroll this. then we can address the concerns of @catch.
Comment #50
YesCT CreditAttribution: YesCT commentedauto merged with no conflicts. (patch 50)
I didn't answer catch's questions, but did fix #48 4, 7, 12 in patch 50b.
Comment #51
sunThanks for the detailed review, @catch! :-)
It looks like all the questions you raised are very minor, but I'm happy to address some of them in code, and clarify on the others:
re 1) Yes, I absolutely agree,
drupal_get_filename()
needs to die. The ultimate goal of the extensions system revamp effort is to get rid ofdrupal_get_filename()
anddrupal_get_path()
and ... and [insert large dependency chain here]... ;-) The discovery patch can only simplify it, but not remove it yet, because the fallback on discovery is only the edge-case situation in that function.re 2) I removed and cleaned up all @todos that do not have to live in code, except of those that required me to uglify the new code.
re 5) Most of the blacklist exists in HEAD already, see
SystemListing::scanDirectory()
. The only additions are "src" + "vendor" (complementing existing "lib" + preparing for PSR-4), "assets" + "files" + "images" + "misc" (complementing "templates"+"css"+"js"), "includes" + "fixtures". I think it is fair to expect and simply say that no extension can use any of these [directory] names. Every unnecessary subdirectory [tree] recursion costs performance.re 6) Off-hand, I'm not able to see a use-case for customizing the additional blacklist. In any case, if such a use-case arises, we can consider to change the code in a separate issue.
re 8) When I first worked on a similar implementation 1-2 years ago, someone raised the idea of renaming it to
config_ui
(in line with field_ui + views_ui). We can consider to do that in a separate issue, but wouldn't be strictly required, as the special filter for "config" works excellently for now (and doesn't decrease performance).re 9) Once the extension system cleanup is done, only two properties will be serialized,
$type
and$pathname
. Everything else can be derived from those two values. — Not sure which comment you missed, but the publicExtension
class properties as well as theserialize()
andunserialize()
methods are cluttered with @todos already? ;) → Once this baseline is in, we are able to move forward on the higher layers, extension list + extension info. Especially the info layer is where various other functions are adding random properties to the discovered extension objects. Dire need of cleanup. That's why I'm working on this in the first place :-)re 10) I've adjusted the comment, but I actually want to remove the
/sites/all
location altogether from D8, because it fully duplicates the top-level directories. Especially when the root /settings.php patch will have landed, that option really doesn't make any sense anymore. The only reason for why we originally kept it was "we're not sure yet."re 11) I'm not going to add a helper method for the info parsing and core version compatibility check, because extension info handling is a separate concern that is not to be addressed by extension discovery; i.e., if you need to perform this check, then you will not look into
ExtensionDiscovery
. — The mere existence of this code in theExtensionDiscovery
class actually offends me to begin with. As a potential separate follow-up issue, I'd like to propose to remove it altogether. → Because with Migrate in core, the edge-case this code is conveniently catering for (a core module being overridden by a later path, but incompatible with current core, since originating from a previous version of core) should theoretically no longer exist.Comment #52
sunFWIW, I did not intend to delete @YesCT's re-rolled patches — yet another bug with d.o's issue update conflict resolution, it seems. :-/
After removing the @todos from the code, I'm adding them "back" to the issue summary now, so I can create those issues after this patch has landed. :)
Comment #53
sunHm. I'm blocked on some of the improvements of this patch in multiple other issues — any chance to move forward here?
Comment #54
catchI've been struggling finding time to get back to this, but had another look through and no more complaints really - or they'll hopefully get resolved in the next set of issues.
However, this no longer applies.
Comment #55
catch51: extension.disco_.49.patch queued for re-testing.
Comment #57
sunMerged 8.x. Simple diff context conflict.
Comment #58
catchCommitted/pushed to 8.x, thanks!
Comment #59
andypostCan't install into multi-site folder
EDIT
sites.php - $sites['d8.l'] = 'd8.l';
Comment #60
catchReverted for now.
Comment #61
andypostThe problem was caused by custom theme that is sym-link'ed to site's theme folder
Comment #62
penyaskitoIt was not introduced here, see #2204675: Extension discovery is not discovering symlinks
Comment #63
catchandypost confirmed this was a regression introduced by this specific patch in irc.
Comment #64
andypost@penyaskito No! it is introduced here, before this patch my theme and modules works fine.
So I see no reason to commit major issue and make another major to fix regression!
I've checked php 5.4 (fresh debian) and core fails too: I just placed symlink to theme folder
Comment #65
niko- CreditAttribution: niko- commented@sun This looks like we must use
http://ua1.php.net/manual/en/splfileinfo.getlinktarget.php
http://ua1.php.net/manual/en/splfileinfo.islink.php
for checking if dst is symbol link and get its real target in this case.
Comment #66
sun:-/ It would have been much easier to resolve the symlink issue in a quick follow-up patch instead of reverting...
I'm developing on Windows, so I'm not actually able to reproduce the issue. (Windows has
mklink
, which allows symbolic directory links and junctions, but they are transparent to PHP; i.e., not interpreted as symlinks.)Unless I'm terribly mistaken, attached patch should cut it. @andypost et al, can you test, please?
Comment #67
damiankloip CreditAttribution: damiankloip commentedThis fix looks good to me - let's let andypost confirm and RTBC.
Comment #68
andypostNow it works, just re-roll because 66 applies with offset.
Also I found that installation of modules become a bit slower but seems this caused by yml parser
Comment #69
catchThanks! Committed/pushed to 8.x.
Comment #70
andypostComment #71
sunCreated the follow-up issues mentioned in the issue summary:
#2209283: Remove /sites/all in favor of top-level directories
#2209293: Remove core compatibility check in ExtensionDiscovery; obsolete with Migrate in core
#2209307: Convert database drivers into a regular extension type
#2209319: Move 'type' .info.yml file property first for performance (lead by example)
Comment #73
Wim LeersThis also seems to have broken
run-tests.sh
on OS X: #2216695: run-tests.sh broken on OS X ("Too many open files").Comment #74
chx CreditAttribution: chx commentedEven for Drupal 8 which is full of stupid ideas this is a spectacularly stupid idea -- how the hell are we supposed to debug test failures if we can't switch on a test module?? Incredible!
Comment #75
sunSee #2198713: Add a settings.php variable to allow to discover/enable test extensions at regular runtime
Comment #76
donquixote CreditAttribution: donquixote as a volunteer commentedHere are new tests documenting the behavior of ExtensionDiscovery.
#2729711: Improve test coverage for ExtensionDiscovery
I wonder if all of this is the intended behavior, or if some of it is by accident.
E.g. I remember that in D7, files in parent directories had priority over those in subdirectories with same name. In D8 it is the opposite.
Also, there is nothing to prevent extensions of different type to have the same machine name.
Comment #77
donquixote CreditAttribution: donquixote as a volunteer commentedIn aftermath of this issue, was it a good idea to let the Extension object pretend to also be the SplFileInfo object for its *.info.yml file via magic __call()?
Why did we do this?
#2959989: Deprecate Extension::__call() magic