Problem/Motivation
We've been working for many months now to improve Drupal 8 performance. But not only is measuring (i.e. "profiling") it hard, it's also hard — even for the most prolific contributors — to not accidentally introduce performance regressions. Currently, it's trivial to introduce performance regressions that go unnoticed: additional services being added for one purpose may in fact be initialized always (even when unnecessary — see #1973618-27: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services")), additional JS assets that are loaded accidentally (e.g. #2160555: Disable Edit on admin pages), and so on.
The only way (currently) to see if a patch negatively impacts performance, is by performing many manual steps:
- XHProf-based profiling requires a lot of labor, hence very few people use it
- alternatively: webprofiler.module, which is a lot simpler, but very few people use it and it's often broken due to changes in HEAD
- generating content is very cumbersome: either you have to do it manually, or you have to wait for
devel_generateto be fixed (it, too, is often broken due to changes in HEAD)
Not to mention that actually making proper comparisons is extremely hard.
It's nigh impossible to do it with XHProf unless you have a computer with a constant, controlled environment — results vary wildly with the environment (hardware, PHP version, system load, etc.)! On top of that: if we're comparing a specific, approximately-real-world-scenario, you need generated content, and it's currently very painful to generate the exact same content in different versions of Drupal 8 (due to changes in HEAD).
Some profiling is better than none: increasing awareness and accessibility
In order for Drupal 8's performance to only move in the general right direction performance-wise from now on, we need to:
- increase awareness and accessibility: when people post a patch that affects performance (in the positive or negative sense), they should be notified
- provide a default set of profiling scenarios in core, so that it's easy for anybody to profile the same scenarios on their systems — this also helps developers onboard to more advanced profiling
Ideally, we'd have an interface to install Drupal (#2115533: Add InstallerInterface to provide a stable non-interactive installer) as well as an API to generate content. Then, anyone could write scripts for the scenarios they care about. But, that's a massive undertaking (which msonnabaum eventually had to give up on) and it still doesn't help with the above points.
Also ideally, we'd be able to automatically run XHProf (but alas, it's never installed by default and in fact for a few months you've needed a custom build of XHProf for it to work with Drupal 8 — none of the packaged XHProf versions work with PHP 5.5 + Drupal 8!), and even more ideally we'd be able to get the number of CPU instructions it took to render a page. Neither of those are achievable. And even if they were, we'd still not have apples-to-apples comparable numbers, because the results depend on the environment (hardware, PHP version, etc.)
In an überideal world, we'd have a PerfBot running in tandem with TestBot. But creating the infrastructure for this is non-trivial, especially because there are more strict environment requirements for a PerfBot, if we want its results to actually be correct.
What is achievable, is measuring numbers that don't depend on the environment and still are good performance indicators: number of CSS/JS assets, cache/config/state gets, DB queries, initialized services, rendered blocks … And to measure those things, we can simply build on top of what we already have in core: http://api.symfony.com/2.4/Symfony/Component/HttpKernel/Profiler/Profiler.htmlSymfony's Profiler! (This is also what Web Profiler uses.)
Yes, those numbers should be interpreted with care as well. There is nothing inherently bad about the number of services or the number of cache gets for a certain response. But it usually is bad when those numbers increase, and all too often that happens without us noticing. So that is what this issue wants to provide. And at the same time, we get profiling scenarios in core that we can
Proposed resolution
Therefore, this issue proposes a simpler set of steps:
- create a 'Profiling' install profile that reuses the 'Standard' profile but adds generated content to it — being done in #2497185: [no patch] Create standardized core profiling scenarios and start tracking metrics for them
- this new install profile would include a
ProfilingTest(much like the 'Standard' profile hasStandardTest), but instead of testing whether things work as expected (which is already covered by the current body of tests), it enables a hidden 'Simple Profiler' module (part of the 'Profiling' profile), which compares the actual number of cache gets, config gets, initialized services … with the expected numbers for various scenarios — initially this will only test peak memory, also being done in #2497185: [no patch] Create standardized core profiling scenarios and start tracking metrics for them - this 'Simple Profiler' module would be a very barebones, no-UI version of Web Profiler, and it would leverage Symfony's
ProfilerandDataCollector(just like Web Profiler) - the test would mark every status quo or improvement as a "pass" and would mark each regression (by 5% more than the expected peak memory, or more than the exact number of initialized services, other metrics TBD). This is how every patch author would be notified of performance regressions.
This is what that looks like (in the old iteration of this patch that requires precise matches, and is this much more of a PITA — screenshot to be updated):

Remaining tasks
Discuss the general idea.If we want to go forward with this, we'll also want to discuss what the specific profiling scenarios should be. We can always add more though!Done at #2497185-20: [no patch] Create standardized core profiling scenarios and start tracking metrics for them.- Step one: 4 scenarios, very minimal metric tracking: #2497185: [no patch] Create standardized core profiling scenarios and start tracking metrics for them
- Step two: DB queries metric, issue to be created, see #19
- Step three: initialized services metric, issue to be created, see #19
User interface changes
None.
API changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #90 | interdiff.txt | 6.63 KB | sorressean |
| #90 | profiling-2308683-90.patch | 65.73 KB | sorressean |
| #87 | interdiff.txt | 822 bytes | sorressean |
| #87 | profiling-2308683-87.patch | 64.41 KB | sorressean |
| #84 | interdiff.txt | 2.19 KB | sorressean |
Comments
Comment #1
wim leersNote: all the
DataCollectors and service wrappers were essentially copy/pasted from https://www.drupal.org/project/webprofiler. Many thanks to lussoluca and dawehner for Web Profiler!The 'Profiling' profile's default config is identical to this:
I've attached a
*-without_config_files-do-not-test.patchfile for easier reviewing.For reviewers:
profiling.installComment #2
wim leersComment #3
dawehnerI love to see work on automatic profiling!
Additional I really like that you have chosen to leverage the abstractions from the webprofiler.
it seems odd to not have this module available in normal core
it would be great to name it @profiler.storage by default, so replacing it is easier.
i know this is a copy and paste but this feels like a bug. This does not respect the result of getMultiple ...
<3 This will ensure that everyone is using the cache factory interface
On the longrun you plan to make it hidden, right?
Cool, unicode!
Comment #4
lussolucaWow Wim, this is awesome!
I think this is the correct approach if we can't put all Webprofiler (data collectors and UI) into core. Because the way the webprofiler is done it would be difficult to separate data collectors from the toolbar and from the backend
Comment #5
wim leers#3.1: I didn't add it to normal core because 1) it has no UI; it can only be used programmatically, 2) it's intended to be hidden (so to #3.5: yes!) — it's specifically designed to be used by the tests. Perhaps I should rename it to
test_profiler. Therefore I think #3.2 is also unnecsesary.#3.3: I agree that this feels bizarre — but as you already say, it comes straight from Web Profiler. I do think it's valuable to register the number of cache hits. But I also think we should record how many actual "cache gets" there are. If we have 50 cache hits, but get them in 20 cache gets, that's important to know.
#3.4: Indeed :)
#4: Great to get confirmation, thanks :)
Comment #6
larowlanCouldn't see it mentioned, does this include memory use?
Comment #7
wim leers#6: not yet, but it could. I fear those results do depend on the environment though: PHP version and CPU architecture would affect this, and possibly the OS used as well.
Comment #8
msonnabaum commentedI haven't dug into the approach much yet, but I'm +1 to the idea in general. It's not the whole picture, but anything to get us started seems positive to me.
Comment #9
Anonymous (not verified) commentedi like the idea of tests that set up 'real world' scenarios - generating data, setting up views etc.
i'd rather we didn't hard-code counts of expected cache hits, but instead did xhprof feature detection, and wrote out dumps.
then we have all the pieces we need for the non-drupal bits to be built around it to get xhprof diffs from D8 clean vs D8 + a patch.
Comment #10
moshe weitzman commentedWe discussed this in the Perf call this week and nobody loves this approach, but we do think it is way better than nothing (i.e. the status quo). So, +1 from me.
A followup that I'd be interested in is a run-profiling.sh script that runs a scenario. Then we could compare xhprof runs pre/post a given patch.
Comment #11
moshe weitzman commenteduser.role.administrator.yml. It will be hard to keep the dupes in sync with Standard profile. Or maybe we are OK with drift here? If profiling profile has to have own config, then maybe we symlink from standard profile? I'd be OK with profiling profile requiring symlink support. PHP+Windows has had support for this since Vista and Server 2008.Comment #12
jhedstrom+1 on this idea.
Moving to needs work as per #11.
Comment #13
dawehnerSome basic ideas to move this issue forward:
Comment #14
wim leersTurning this into a meta, we should get this done, in baby steps. @dawehner and @amateescu are taking this on.
Comment #15
dawehnerWork in done on http://cgit.drupalcode.org/sandbox-chx-1857558/log/?h=2308683
Comment #16
dawehnerI hope amateescu manages to find some time to resetup his configuration again :( :( :( :(
Comment #17
webchickIf that happens, could I make a small request, which is to spin it off into a proper project, rather than a branch of a core sandbox? While we'd want to move this back into core eventually, in the interim while it lives in contrib, it would be much easier to test if it was a matter of
drush dl foo.Comment #18
webchickReviewing #2497185: [no patch] Create standardized core profiling scenarios and start tracking metrics for them, these all seem to use standard as the base. Looks like Wim's patch does as well. Seems like this would be easier if we had #1356276: Allow profiles to define a base/parent profile. Then no need to keep this profile and Standard in sync all the time.
Incidentally, there already exists https://www.drupal.org/project/profiling as well as https://www.drupal.org/project/profiler which are something else.
Since good namespaces are scarce, for giggles, I added https://www.drupal.org/project/zoom in the vain hopes I'd have time to work on this this evening. :P Added catch, wim, dawehner, and amateescu as co-maintainers. Happy to kill the project though if that's not helpful.
Comment #19
wim leersPer #2497185-20: [no patch] Create standardized core profiling scenarios and start tracking metrics for them, where we also agreed upon the (initial) set of scenarios to profile:
Doing steps 1 and 2.2 in #2497185: [no patch] Create standardized core profiling scenarios and start tracking metrics for them. Will file child issues for 2.2, 2.3 and 3 later.
Comment #20
wim leers(#19 accidentally removed the "meta" prefix, oops!)
Quoting @catch at #2497185-22: [no patch] Create standardized core profiling scenarios and start tracking metrics for them.
Comment #21
sorressean commentedI've removed a lot of what doesn't actualy belong here and started narrowing this down. Once this finally installs I can finish up adding the other required metrics.
This is currently failing after install; the error is as follows:
Comment #22
tim.plunkettMight as well let the testbot have a crack at it
Comment #24
wim leers#21: I'm pretty sure that's happening because
profiling_install()puts the first ten nodes in themainmenu, yet that menu does not exist. It does not exist because the "Profiling" install profile in this patch does not include any config to be installed. The patch in #1 does. The patch in #1 basically copied all config from the "Standard" install profile. I think you want to do the same here. (And perhaps then modify it going forward, but that's a good starting point at least.)Comment #25
sorressean commentedUpdated to include profile configuration as suggested in #24, also fixed syntax errors I always forget to check for.
Comment #26
sorressean commentedComment #28
sorressean commentedUpdated to fix some exceptions and fixed profile.install.
Comment #29
sorressean commentedComment #31
sorressean commentedI updated this to work with the most recent commits. Same issues on my tests: field issues which will probably show on next tests. I'm not sure how to fix this, so any help here would be awesome.
Comment #32
sorressean commentedComment #34
wim leersI'll take a look at fixing #31. Will assign to me when I actually work on it.
Comment #35
wim leersThis should fail with a single exception: for the single assertion that the newly added test adds.
Comment #37
sorressean commentedUpdated to insure that it passes profile test. Adding more metrics shortly.
Comment #38
sorressean commentedComment #40
sorressean commentedtrying again--sorry about readme.
Comment #41
sorressean commentedComment #43
sorressean commentedFixed exceptions above, also added SQL metrics. This may or may not be wrong. Any ideas on how to handle SQL queries better would be awesome.
Basic idea:
The logger holds a log of all queries. Solution: just use the size of the array, which will return count.
Comment #44
sorressean commentedAdded the correct profiling patch this time...
Comment #45
sorressean commentedComment #48
dawehnerLet's adds some form of docs
Let's use count() everything else is confusing, IMHO
Comment #49
wim leers#43 introduced a separate scenario to profile SQL. I don't think this makes sense: it requires us to render the exact same pages again. Why not collect that the first time around? This makes the test significantly slower.
In fact, it looks like this SQL data collector that you added made tests several orders of magnitude slower. It seems to send tests into some sort of recursive installation loop. I think because it's collecting the function call stack as well.
#19 mentioned 3 things: peak memory, SQL queries and initialized services. To help get this patch back on track, I'm adding the initialized services part back (it already was in #1), freshly copy/pasted from the
webprofilermodule.I've added the
DatabaseDataCollectorfrom Web Profiler instead of the custom written SQL data collector. I'm not yet using that data collector in this patch though, because there's something interesting about it. See the next reroll.I've also added a warm cache scenario, which shows how this was intended to be used.
This does not belong in the install profile, but in
\Drupal\Core\Profiler, like my patch in #1 already did.How did you end up with this list of modules to install? This is not at all a copy of the Standard install profile, and hence it's not actually testing something we want to be testing.
The
simpletestmodule for example never makes sense to enable when profiling.And the
page_cachemodule is conspicuously missing, without explanation. (It should not be enabled because it is a middleware, during which no events are fired, and hence no profiling is possible. But that reason for its absence should be explicitly documented, and now it is.)s/profilele/profile/
s/variousus/various/
And generally outdated.
Plus, factually wrong, because this does not profile scenarios. A module can never profile scenarios, only a test can. This is a module that simply gathers profiling data.
Comment #50
wim leersAnd now with query counts being measured. Note how the peak memory numbers increase drastically (from ~6% more than the expected number of 32 MB, to ~34% more). This makes sense: collecting all that query information causes the memory usage to increase.
I don't think there's a way around this. But … that really, really sucks. It's because the measuring logic and the measured logic both are part of the same problem. I don't think we can solve this.
Either we have some heuristic to subtract some number of memory bytes based on the number of logged queries, or we simply cannot measure database queries. (Or we need to update the DB API to provide a much more efficient way of measuring the number of queries.)
Comment #51
wim leersAnd here's some clean-up plus todos.
Comment #55
sorressean commentedUpdated todos, thanks Wim. We should be set now short of the metrics.
Comment #56
sorressean commentedComment #57
sorressean commentedComment #59
sorressean commentedComment #60
sorressean commentedComment #61
sorressean commentedComment #63
wim leers@sorressean: Can you please post the interdiff together with the patch next time? d.o supports attaching multiple files per comment. Or is there an accessibility problem with that perhaps?
So, all expected numbers have changed, which is why there are 6 exceptions. That's an easy fix :)
But, what's concerning is that the numbers are so vastly different. Let's first get a green patch, and let's then upload the same patch ten times in a single comment. That'll run the same tests ten times, so we'll see if the measured numbers are sufficiently consistent for this test to be reliable.
Comment #64
sorressean commentedThanks, wim. I'm not really sure what the problem was (accessibility or not). I found a workaround to the problem though, hopefully.
In this patch:
* Updated values to match with the test.
* Added an identifier to the format, so that 75.5 would show as 75.5M.
* 64 is a magic number (this comment), lets hope this works!
Comment #65
sorressean commentedComment #66
sorressean commentedInsuring that we get the same results.
Comment #67
sorressean commentedComment #77
sorressean commentedThese show that there is not a solid query count that can be confirmed. Although there may be a variant of a small percentage (lets say 5), higher deltas (~20+%) suggest that there might be caching issues that need to be examined.
The only other issues seem to be deltas in memory usage, although those seem to be within small ranges, so I would hazard to mark those as acceptable.
I'd also really like to find a good direction to go with this from someone who knows a lot more. Thoughts?
Comment #78
wim leersOn a call with @effulgentsia, @sorressean and I, we decided to decrease the scope that was defined in #19: rather than doing peak mem, DB queries and initialized services, do only initialized services. That's still a great first step, still very valuable, but doesn't suffer from any of the problems with peak mem and DB queries.
(Problems with A) peak mem: opcache differences between testbots make it very unpredictable, infra improvements necessary, B) measuring DB queries, and the way that that is supported by doing extensive logging, significantly affects peak mem.)
Comment #79
sorressean commentedHere's a patch without the extra collection of data--only service count:
Comment #80
sorressean commentedGo testbot go!
Comment #84
sorressean commentedRemoved services (oops), also did some basic cleanup to make the patch apply cleanly.
Comment #85
sorressean commentedComment #87
sorressean commentedI was stupid and forgot to decrease the expected service count...
Comment #88
sorressean commentedComment #89
wim leersThis comment is rather pointless, since the service tag already indicates that. Let's remove it.
Missing docs.
Missing docs.
Incomplete docs.
Incomplete docs.
Outdated.
Missing docs.
Actual docs needed.
Incomplete docblock.
s/currenty Includes/Currently includes/
This needs to be moved into a separate file and needs some serious cleanup.
Let's do this now.
Incomplete docblock.
Incomplete docblock and indentation problems.
Also: s/plane/plain/?
Let's use PHP 5.5 short notation array syntax.
Comment #90
sorressean commentedI got into some bad habbits and just kept hacking on this without bothering to update any docs. Thanks for the embarrassingly long list.
I left out scenarios since we want to keep this simple. Those can easily be added later.
Hopefully this is a lot closer to what was needed!
Comment #91
sorressean commentedComment #93
moshe weitzman commentedWe discussed this in the D8 Perf call today. In order to get this to green, we scoped this patch down to counting just service instantiations. Thanks especially to @sorressean for getting us there. The patch adds a near duplicate of standard profile, with all its config files. Thats going to be a bit painful to maintain. When this profile can start delivering more counters than service instantiations, lets consider committing it.
In some other issue, the register_tick_function was suggested as a way to do function counting without an external extension. Is worth exploring here.
Comment #94
cosmicdreams commentedDid this miss the boat or are we still going to ship this installation profile with Drupal 8.0.0?
Comment #95
moshe weitzman commentedMissed the 8.0.0 boat. Could be added in a minor release.
Comment #96
sorressean commentedI missed this so much I'm going to start going after it again... :) It obviously missed the d8 release, but this could still be useful. to my thinking, we need to do a couple things; the code is failing although I think I fixed all of the stuff Wim pointed out. So we just need a few more metrics. I think the hangup on this was that the testbots were reporting drastically different issues; at one point testbot was noting memory errors while QA was noticing querie errors so we started getting some weird results out of this.
Comment #97
moshe weitzman commentedGlad to hear it, @sorresean!