Problem/Motivation

JavaScript string translation, and particularly the discovery of the translatable strings, was designed for the D5/D6/D7 asset handling system's lowest common denominator, which was: drupal_add_js(). This function allowed you to add any JS asset to be loaded at any time during the request.

In Drupal 7, we have a list of JavaScript files in library definitions, if not all files on a site. This allows some JavaScript parsing to happen on rebuild, and should reduce variable_set() calls after cache clears.

Proposed resolution

On js/css cache rebuilds, collect all js files from library definitions and .info scripts[], then parse them and write to the variable once.

This will help to avoid variable_set() stampedes.

Remaining tasks

User interface changes

None.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Gábor Hojtsy’s picture

What would you do with possibly dynamic asset libraries. That is a possibility, locale is actually exploiting that in #2419897: Javascript translations are loaded in the wrong order due to missing dependencies :)

Wim Leers’s picture

hook_library_info_build() is fine.

But … I suspect you're pointing at hook_js_alter(), which could possibly swap one JS file for another? Yes, that would not be picked up, that'd break things. Modules doing that kind of batshit crazy things should just create a library with all those hyperdynamic assets, for locale module to discover strings in, and then not attach that library.

IMHO the only valid use case for swapping JS files is locale module. If many modules would do this kind of thing, that'd also totally destroy front-end performance because it'd cause different aggregates on every page. Basically: using hook_js_alter() requires extreme care. And it's now necessary far less often than when it was introduced, because we now have libraries, dependencies, and dynamic libraries.

I personally can't think of other valid use cases for hook_js_alter().

Gábor Hojtsy’s picture

What would you do when you encounter such an altered JS list situation that "people should not do"? I mean if its part of the API someone will have brilliant ideas :) Eg. config schema recently got exceptions if someone uses the schema altering hook to remove or add schema items (rather then actually altering them) so developer creativity is sandboxed in where it was supposed to be.

Wim Leers’s picture

Version: 8.0.x-dev » 8.1.x-dev
Issue tags: +API clean-up

Agreed.

Besides, this is not in anyway blocking for 8.0. It'd constitute the tiniest API change. So, postponing to 8.1.

catch’s picture

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

This isn't blocking but I don't see why we shouldn't do it prior to release if we can.

catch’s picture

Adding #1677026: Less aggressive locale js file rebuilds as a related issue

I think we should go ahead with this approach in 8.x, so I'm moving that issue back to 7.x now.

catch’s picture

catch’s picture

#1677026: Less aggressive locale js file rebuilds is the issue, trying once more with the related issues field...

catch’s picture

I think we could backport some of this logic to 7.x:

- when refreshing the cache, instead of just nuking the entire javascript_parsed variable, check hook_library() for every module, then parse those js files.

This would mean a lot of parsing moves to the process invalidating the cache, making things a lot less stampede-y. We'd need to leave in the rest of the dynamic logic though.

catch’s picture

FileSize
935 bytes

Here's a 7.x proof of concept. This pre-seeded the javascript_parsed with over 80 files on a real 7.x site for me.

Fabianx’s picture

The Proof of Concept looks great to me.

catch’s picture

Version: 8.0.x-dev » 7.x-dev
Status: Active » Needs review
FileSize
935 bytes

Actually going to run this past the bot, since on the site I'm testing with there are files missing from core libraries.

heykarthikwithu’s picture

+  foreach (module_list() as $module) {
+    foreach (drupal_get_library($module) as $name => $library) {
+      if (!empty($library['js'])) {

minor update on this, can we remove the unused variable $name in the for loop?
since we are not using this $name variable.

+  foreach (module_list() as $module) {
+    foreach (drupal_get_library($module) as $library) {
+      if (!empty($library['js'])) {

Status: Needs review » Needs work

The last submitted patch, 12: 2421323-7.x.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
FileSize
1.48 KB

Removed the unused variable.

Added support for scripts[] in themes.

Added support for misc/drupal.js since that's already hard-coded in core and not in a library.

Status: Needs review » Needs work

The last submitted patch, 15: 2421323.patch, failed testing.

Fabianx’s picture

Fatal error: Call to undefined function _locale_parse_js_file() in /var/lib/drupaltestbot/sites/default/files/checkout/includes/common.inc on line 5108
Drush command terminated abnormally due to an unrecoverable error. [error]
Error: Call to undefined function _locale_parse_js_file() in
/var/lib/drupaltestbot/sites/default/files/checkout/includes/common.inc,
line 5108

is what PIFR reports.

catch’s picture

Status: Needs work » Needs review
FileSize
1.8 KB

Added a require_once for that.

Also handling scripts[] from module .info files. That feature's gone in 8.x but still in 7.x.

Status: Needs review » Needs work

The last submitted patch, 18: 2421323.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
FileSize
1.93 KB

Not enough. Despite being in locale.inc, _locale_parse_js_file() depends on locale module.

Don't like it, but added a module_exists(). Would be cleaner to move some of the logic to locale module but that would require at least an API addition so it can implement a hook or something.

Fabianx’s picture

Assigned: Unassigned » Gábor Hojtsy
Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs subsystem maintainer review

Would be RTBC, but 8.0.x needs to go in first - unless we want to split based on code now in classes.

Pinging @Gabor on it anyway.

catch’s picture

While the concept is the same, I don't think there's any shareable code here between the versions at all. hook_library() has changed beyond recognition, module .info files can't have scripts, themes js definitions changed too. So the 8.x patch will need to be done from scratch, and it's the kind of change that might only be possible in a minor version too.

Also this patch doesn't change the runtime logic at all - that has to be kept for backwards compatibility in Drupal 7. However in 8.x since everything is in libraries, the runtime logic / hook_js_alter() can be retired completely.

And even further, 8.x doesn't suffer from variable_set() stampedes, because variable_set() has been removed completely. So while there's still a potential performance benefit moving from runtime/on-demand parsing to rebuilds, it's less severe, and more to do with simplification of the overall logic and the patch being in 7.x won't constitute 8.x having a performance regression if that lands first.

So with my 8.x maintainer hat on (work on this patch was for a client), I'd be in favour of different 7.x and 8.x patches and issues for this.

Fabianx’s picture

So if the 8.0.x Release Manager agrees, this should be split: Great! I am of the same opinion.

So lets split a new issue out for 8.0.x and get this one in for 7.x? :)

Gábor Hojtsy’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: -Needs subsystem maintainer review +Needs issue summary update

Looks good. Makes sense based on the explanation in #9 :) Needs issue summary update, so the committed patch makes sense vs. the summary. The summary still seems to be about 8.x, while the issue abandoned 8.x.

Only minor note is this line going over:

+++ b/includes/common.inc
@@ -5078,7 +5071,51 @@ function drupal_build_js_cache($files) {
+    // Hard code drupal.js since it is hard-coded in drupal_add_js() and is not a
Gábor Hojtsy’s picture

Assigned: Gábor Hojtsy » Unassigned
catch’s picture

Title: JS translations should be generated in their entirety, for better performance, less race conditions » Parse js files from library definitions during rebuild to minimize variable_set() calls
Issue summary: View changes
catch’s picture

Status: Needs work » Needs review
FileSize
2.55 KB

Fixing that comment over 80 chars.

Also added a file_exists() around _locale_parse_js_file() to avoid errors with modules like jquery_ui that specify files that they don't actually provide in library definitions.

While those are bugs in the modules, suddenly having PHP warnings on cache clears makes it look like we're introducing a bug here.

catch’s picture

FileSize
1.97 KB

cruft.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs issue summary update +Needs tests

All right. I think it looks good. Ideally this would get some test coverage, if there is a reasonable way to do so.

joelpittet’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Needs work
FileSize
210.82 KB

This has a pretty negative effect on clearing cache performance. Was that intended?

From a drush cc all before and after the patch I'm getting 24ms to 440ms jump.

I've got a lot of modules (commerce site) which may be part of the reason but I thought I'd share before this gets committed since it has Performance tag.

catch’s picture

Yes that's by design. The actual cache clear will take longer because all available js files are parsed. However the requests immediately after the cache clear save both the file parsing and the individual variable_set() calls, which in turn saves a lot of variable_init() cache misses on the requests after those.

joelpittet’s picture

Issue tags: +needs profiling

Ah, we should get a profile on that then, thanks for clarifying @catch.

catch’s picture

This isn't so much about profiling though - it's more about avoiding a lock stampede in variable_init().

Let's say variable_initialize() takes 100ms to get all variables from the database, unserialize() them and write to cache. This varies a lot depending on how many variables are in the database but I've seen it take 100ms on sites with over a thousand variables.

Looks a bit like this:
Cache is cleared.
Individual requests find js files to be parsed, parse them, set the variable.
Every request that comes in during the next 100ms either has to rebuild the variable cache or hits a lock_wait() pattern in variable_init()
Potentially, the first request to continue finds another js file that hasn't been parsed yet, and starts it over again. Some of the requests in lock_wait() get stuck on the second lock too.

So the ~100ms it takes to rebuild the variables on a large site needs to be multiplied by the number of requests that hit during that rebuild, and the number of times the rebuild is triggered by subsequent variable_set() calls.

One way to get numbers on this is to log the $name argument to variable_set() on production for a day or so (including after a cache clear).

joelpittet’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -needs profiling

I've run into this personally, I think I killed/staved off by patching most modules with large variables to stop seeing this stampeeding.

It was painful to debug when this happens, so I'll set back to RTBC.

David_Rothstein’s picture

Status: Reviewed & tested by the community » Needs work
FileSize
666 bytes
  1. +  $dir = 'public://' . variable_get('locale_js_directory', 'languages');
    +  file_unmanaged_delete_recursive($dir);
    

    I tested the patch and this broke things for me. It deletes all the files, but (unlike _locale_rebuild_js()) does nothing to recreate them.

    To see the problem, you can apply the attached testing patch, add a language (e.g. Spanish), and add a translation for the word "Node". Then clear caches via Drush and visit the homepage in Spanish as an anonymous user. With 7.x everything works fine and you see the Spanish translation at the bottom of the page. With the patch from this issue applied you start seeing English.

    I think it's possible everything will work OK when combined with #1677026: Less aggressive locale js file rebuilds, but if so we have to either postpone this issue on that one or combine the two.

  2. +  variable_del('locale_js_files');
    

    Relatively minor, but I can't find this variable used anywhere else in the codebase?

joelpittet’s picture

@David_Rothstein thanks for the steps, sorry the test site has two english regions en-US and en-CA so I didn't run into that:S I'll have to dig deeper.