Split-off from #1578090: Benchmark/profile kernel.

t() got slower, especially when not actually using locale() mostly due to the switch to language(), which is a much slower way than the low-level global $language that we had before.

On my front page with two nodes and the default blocks, I see 159 calls to t() (5% inclusive call time), the most relevant/weirdest callers are:

- system_region_list() is the clear winner, with 51 calls to translate region labels which are not shown anywhere (as far as I know)! :) This call alone makes up for 25% of the time t() takes. Might be relatively easy to "fix" this, it's a dynamic t($label) that we maybe can just get rid of or move into the block UI somehow?
- _menu_item_localize() is up next, with 36 calls. Another 21%. I guess we can't get easily rid of that one, maybe making sure that we only call it for menu items that are actually going to be rendered, I think it's sometimes also run for callbacks?
- 6 calls from system_stream_wrappers()
- 12 calls from image_image_effect_info()
- Also fun: 3 calls from theme_status_messages() even though no messages are shown...

On admin/config, I'm seeing 110 calls from _menu_item_localize() and 30 from system_requirements()

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

Clarification: The above numbers are when logged in as user 1. When being anonymous, the calls to t() on the front page drops to 88 (still 51 of those from system_region_list).

effulgentsia’s picture

Note that we may be able to get t() faster by:
- Compiling the DIC: #1706064: Compile the Injection Container to disk.
- Creating a Translation class/service that depends on the language manager service (so it's auto-reinitialized in subrequests) that could get the interface language in its constructor rather each time t() is called.
- Add a translateMultiple() method to that class, so we can do a bunch of strings in one function call.

However, I think this issue makes sense regardless. Calling t() on something and then throwing away the result seems like an inefficiency worth cleaning up.

cosmicdreams’s picture

Ha!

system_region_list actually has only one t() function.

function system_region_list($theme_key, $show = REGIONS_ALL) {
  $themes = list_themes();
  if (!isset($themes[$theme_key])) {
    return array();
  }

  $list = array();
  $info = $themes[$theme_key]->info;
  // If requested, suppress hidden regions. See block_admin_display_form().
  foreach ($info['regions'] as $name => $label) {
    if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) {
      $list[$name] = t($label);
    }
  }

  return $list;
}

Do we REALLY need region names to be translated? And if we do, can't we just use the value that is defined by the theme. If you want it translated change it there. It's YOUR theme after all.

pounard’s picture

Indeed we want to translate region names, but in administration screens only, system_region_list() is called on every page and not even cached, which means that modules using it will trigger those t() calls (even strings are cached, it's not really a good practice).

Berdir’s picture

Status: Active » Needs review
FileSize
2.51 KB

Not sure if we want to use a different approach and move the t() handling outside of the function (system_region_list_translated() ?) but here's a minimalistic patch for that part.

Brings us done to 37 calls to t() for the anomyous user.

Sitting in the train, not exactly an ideal environment to do performance tests, will try that later.

pounard’s picture

I don't like having boolean parameters everywhere, it's not a really good practice, I'd prefer the 2 functions approach instead: one of them being a pure utility function for rendering or guessing regions, without any human strings returned.

Berdir’s picture

Yeah, I agree. Just returning the keys (an array of the keys) makes sense because all cases that aren't using the translated labels don't use the labels at all, e.g. by doing an array_keys().

I just wanted to get started, I think this is a nice case to be able to test the difference of more/less t() call to see if it's a measurable overhead and the implementation doesn't matter that much for that part.

Did a test with ab -n 1000 -c1, used a high number of requests to get some more comparable results with less variation.

Before patch:
24.90 req/s
40.16 ms

24.98 req/s
40.03 ms

25.04 req/s
39.93 ms

mean: 40.04 ms

With patch
25.18 req/s
39.72 ms

25.21 req/s
39.66 ms

25.17 req/s
39.72 ms

mean 39.7 ms

So this is ~1% improvement, which is small but measurable. Of course that number will go down once/if we make t() fast again.

pounard’s picture

Yes of course, but as a good practice I think it's good to remove unnecessary calls whenever they happen (t or any other function).

sun’s picture

Issue tags: +Performance

Hm. I guess that my proposal from some time ago would actually fix this:

#1213510: A modern t()

The idea is to defer the actual translation to the time when a string is actually printed - until then, the original string including arguments and context remains available as a very slim and simple object (which even allows people to alter it, fwiw).

pounard’s picture

I don't think so, it just would change the t() function to some other mecanism, but it wouldn't eradicate unnecessary t() calls.

cosmicdreams’s picture

IMO, performance optimizations like these would be best considered after Feature freeze.

fgm’s picture

Status: Needs review » Needs work
FileSize
6.38 KB

I've been toying with an idea for deferred translation with static caching. Patch if (very) far from passing, but the idea might be onto something by deferring translation as much as feasible, so I'm putting it up before forgetting about it.

Basically, the idea is to make t() return an untranslated object, and only perform the translation when the result is actually used as a string, be it for a cast, append or whatever operation requiring a string.

Translations are currently kept in a static cache in case they can be reused. This would probably be more efficiently located in locale, possibly in the LocaleLookup class, which already performs translation static caching.

Checking the kind of errors the code returns shows that in some places we tend to make not-really-warranted assumptions like "if this is an object, it has a $option property" (_form_options_flatten(), form_select_options()), or "it is safe to invoke strpos() on an untyped variable (WebTestBase::assert[Raw|NoRaw|TextHelper]()), which might be worth changing regardless of this issue,

In the same vein, the apparently useless implement of printed() and render() are needed because some code, notably in Template, assumes that any object it receives has such a method, and the apparently useless ArrayAccess implementation is needed because of similar assumptions that any non-string is an array (ajax handling in forms).

effulgentsia’s picture

Per #9 and #10, how about moving #12 to #1213510: A modern t()? I like the spirit of it, and possibly if we get that in, we'll find that t() itself becomes trivially fast enough that this issue becomes low priority, but I think it's still a separate issue.

Berdir’s picture

While I hope that we can make t() faster again, let's try that in the other issue. There's nothing wrong with removing 50 unecessary() calls to t(), no matter how fast it becomes.

The attached patch simplifies system_region_list() to only return an array of region names and introduces system_region_labels(), which returns an array of labels keyed by the name.

Berdir’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, system-region-list-labels-1759306-14.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
3.63 KB
10.92 KB

Oh wow, that was messed up quite a bit. This one should be better.

pounard’s picture

Berdir++

sun’s picture

This looks great.

1) I wonder whether it would make sense to have system_region_list() return the region names as both keys and values, so we could use the much faster isset() instead of in_array() in various places?

2) I guess this patch will break #1535868: Convert all blocks into plugins — so I wonder whether we want to postpone it on that?

tstoeckler’s picture

If this is shown 51 times for an anon user on the front page, that warrants a static cache in system_region_list(), right?! That's probably a follow-up issue, but I wanted to ask in case I got something terribly wrong.

pounard’s picture

@#12 I don't like the idea of having a lazy init t object: t backend is supposed to do itself caching, if it needs to (systems such as the PHP gettext library don't need it at all, since the compiled mo files are in PHP shared memory anyway, just like APC bytecode or user cache). Doing this introduces a second caching layer, and it's getting closer to potential cache bloats.

I really prefer no caching at all, reducing the unnecessary call in a pure algorightmic manner, then once done and if the modern t patch comes to agreement, let the backends handle their own cache to avoid double, triple, quadriple caching (real cache bloat). Let's not forget that today Drupal specific locale layer already does a very aggressive caching and uncessary t calls are not costy because it needs to fetch data, but because there is too many of them.

Drupal is almost subject to cache bloats already at some various points, let's not make this even more serious than it is.

EDIT: We have to reduce algorithmic complexity at the very best we can. Optimization can come later, but must not come before, because we will introduce unnecessary duplicated caching layers and algorithmic complexity instead of trusting the layers below (OS cache, PHP cache, APC cache, anything cache below us, which will be a lot more efficient at the end of the day) and the upper layer (smart algorithm, function signature and software design homogeneity, etc..., trust the developers themselves for writing good features and using correctly our API's).

fgm’s picture

@pounard thanks for looking into it, at least. Your observations do make perfect sense.

Berdir’s picture

@tstoeckler: There are three calls to that function, which results in 51 t calls within it. I don't think a adding a static cache to that function is necessary, we're not doing slow calculations, the only problem were the amount of t() calls and that is solved with this patch.

Yes, returning array($name => $name) sounds useful, will update. And yes, I'm fine with postponing this.

tstoeckler’s picture

@Berdir: Right, that makes sense. I thought the function itself gets called 51 times. Thanks for the explanation.

Fabianx’s picture

Looks good to me ...

fgm’s picture

Rethinking my #12 patch and @pounard's observations, this is not so much about caching than about lazy evaluation, actually.

But this bears no relationship with Berdir's patch.

pounard’s picture

I am all in for lazy instanciation, but I don't want it to make code more complex than it is. I'm thinking about pluggability among all other things: if the we want to make the locale system pluggable, we cannot rely easily on such lazy mecanism because the implementation provider may consider than getting the string could be faster than the complex lazy init mecanism.

Just an a example I'm attaching a D7 patch I used on a project (which seem to work, not always, but in most cases) and that saved a lot of time in some pages (for example, a Commerce site with a complex front page can go up to 3,000 t() calls, using this patch I ended up with only 100). Please consider I'm absolutely not pleased with this method and patch that I consider as an ugly workaround.

EDIT: Ignore this patch tests.

The last submitted patch, core-implicit_tstring_object.patch, failed testing.

The last submitted patch, 17: system-region-list-labels-1759306-17.patch, failed testing.

Fabianx’s picture

jhedstrom’s picture

I'd be happy to reroll #17 if folks think that's still the way to go on this.

Berdir’s picture

I guess this would still be useful, although the API change is probably not acceptable anymore, so we might need to find a better way to do this.

David_Rothstein’s picture

Looks like the system_region_list() part of this was handled for Drupal 8 in #2497259: system_region_list() unnecessarily translates region names.

andypost’s picture

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

ndobromirov’s picture

The part with stream wrappers as mentioned in the issue description and maybe others.

Berdir’s picture

Status: Needs work » Closed (won't fix)

The thing is that we now always lazy-translate everything. t() just creates a small object, it is only translated when actually printed out on a page.

So the performance gain from not calling t() is going to be tiny now, so IMHO it's not worth keeping this open.