I Found interesting bug with drupalSettings.ajax overflow. This is the drupalSettings.ajax object, printed from a page with ajax form, after some amount of ajax operations.

When you have an ajax form on the page, and when some ajax operation happened, Drupal send new generated form elements IDs to the drupalSettings.ajax array. Is suppose that this is related to HTML IDs are now randomized for AJAX responses
Do we need some kind of client-side invalidation? Does this problem will be solved by using data attributes instead of global variable? (See #1090592: [meta] Use HTML5 data-drupal-* attributes instead of #ID selectors in Drupal.settings)
I didn't find any other bugs related to this, on frontend or backend.
But if you have a heavily used ajax form (for example ajax filter, like exposed views filters), your page in browser will be larger and larger, and finally crashed, so this is major.
| Comment | File | Size | Author |
|---|---|---|---|
| #51 | core-js-ajax-2561619-51.patch | 705 bytes | nod_ |
| #50 | ajax-error-2.png | 60.98 KB | edurenye |
| #50 | ajax-error-1.png | 169.13 KB | edurenye |
| #45 | core-ajax-cleanup-2561619-45.patch | 3.07 KB | nod_ |
| #35 | interdiff.txt | 3.13 KB | nod_ |
Comments
Comment #2
andyceo commentedComment #3
nod_Comment #4
nod_Comment #5
andypostSuppose code that executes commands should remove the item from that array after processing
Comment #6
andypostComment #7
wim leers+1
… but only if the element is removed from the DOM. In other words: we have
Drupal.behaviors.AJAX.attach(), but notDrupal.behaviors.AJAX.detach(). If we'd add adetach()implementation, this problem would be solved automatically.Comment #8
phenaproximaComment #9
phenaproximaFirst attempt...
Comment #10
hog commentedPatch #9 tested:
1. Afer ajax call repeatedly DOW crowning.
2. According to this issue https://www.drupal.org/node/1090592 we must have data-attribute instead of ID. But we have data-atribute & ID.
Comment #11
hog commentedComment #12
tic2000 commentedYou need to review the issue at hand. The fact that AJAX inserts divs inside divs or the lack of data attributes is not in the scope of this issue.
Comment #13
phenaproximaSecond attempt! Tested manually and it seems to work...
I wasn't sure how to use detach() for this, since the code I wrote needs to run after the AJAX stuff has completed. So instead I added a clean() method to Drupal.behaviors.AJAX, which is called in the attach() method.
Comment #14
lokapujyaTested on my machine. The keys were not being deleted. I noticed that the key variable is a index, but the keys in the ajax object are the selector names. So maybe it should be
delete ajax[value];Comment #15
lokapujyaOther than that, the patch in #13 works. I fixed the problem mentioned in #14. I just don't know if we should use detach() instead of calling clean in attach();
Comment #16
tic2000 commentedIMO it should be in detach, that's the whole reason of its existence.
In detach it will only run when content is removed from the page and that's when you need it to run.
If it's in the attach it will run every time content is added, even if nothing was removed and maybe nothing will be removed.
Comment #17
lokapujyaI uploaded the wrong patch in #15.
Comment #18
phenaproxima@tic2000: If you look at the code, you'll see that it cleans up by determining if the AJAX element actually exists on the page. So the question is -- does detach() run before the element has been removed, or after?
Comment #19
tic2000 commentedI tested this patch. If it's in the attach it gives false positives because the element may be added to the page later than ajax.js attach.
I also tested with the code moved into detach.
I clicked on the links for "Field Type" for body, comment and dummy text. Then on Edit and Delete for dummy text. All of them are ajax links and add a form to the page. Which means 5 in total as in first image.
This is the delete form with the submit also using AJAX. It removes the field, but more importantly for this issue, the 3 forms related to dummy field.
After the click we see that only 2 objects remain in drupalSettings.ajax which is what I expected.
Comment #21
droplet commentedIt can be done with this way. (or after AjaxCommands is executed)
Is it true all time ?
Comment #22
phenaproximaOne problem with the patch in #21 is that it will only be executed if a
settingscommand comes back from the server. If one doesn't, drupalSettings.ajax will continue to grow and grow. I feel like the cleanup should be done as often as possible, but please correct me if I'm (probably) wrong about that :)Comment #23
tic2000 commentedDrupal has detach for a reason. If you put the code in detach you know it will be run when something is removed and not every time an AJAX call is made, even if it removes nothing from the page.
Comment #24
lokapujyaIs it ok that each element is looping through AjaxSettings? That will guarantee no leaks, but couldn't each element clean up after themselves? It probably doesn't matter performance wise, but it could. I don't think I've used enough AJAX that it would matter.
Comment #25
tic2000 commentedThis are added by Form API every time it renders an element with ajax attached. Detaching after themselves is possible in theory, but not that easy and would mean a lot of duplicated code in contrib and in core. For example file module in core would need to check the id's added by uploads and remove them when a user decides he want to remove. The contrib media module would need to do the same. field_ui in core for the widgets on the manage display pages, etc.
Comment #26
phenaproximaMoved into detach().
Comment #27
phenaproximaAdded a small protection against the possibility that drupalSettings.ajax will not exist in detach().
Comment #28
tic2000 commentedI tested this as per #2561619-19: Drupal Ajax objects and settings grows endlessly.
Comment #29
alexpottTo me this looks like a bug that should be fixed in 8.0.x as well as 8.1.x - @andypost was there a reason to move this to 8.1.x?
Assigning to nod_ for a javascript sub-system maintainer review.
Comment #30
droplet commentedEDITED
Drupal.behaviors.detach is a standard way to detach things from Drupal.behaviors.attach. I don't think it's suitable in this case.
I'd expect Drupal.behaviors.Ajax.detach will be detached all Drupal.Ajax stuff. eg:
Drupal.Ajax things binded from loadAjaxBehavior(). Instead of calling it as `Detach`, personally I'd say this is`Swapping over the setting (due to bad design of Drupal Core)` (although Drupal Core does not sync it)
We needed another feedback from Ajax & JS Maintainers.
Furthermore, no matter put the code in where, I think the cleanSettings function in #21 is more effectively and readability. and in #21, it skipped all new attach drupalSettings. I think that could be in some way `detach` is called before the elements insert into DOM.
for example, this line should not exist. If nothing, we do not execute it.
Comment #31
tic2000 commentedDoing things on attach means you run them all the time. On top of that as I said in my previous review you will get false positives cause the settings is the first of the commands. Only then new content is added, so you end up with id's removed that shouldn't be removed and I don't thing doing that in a timeout helps, but even if it did, why do it when we have detach?.
.detach() is the cleanup for attach. Yes, detach should detach all Ajax stuff for the piece of content removed. It doesn't cause it didn't exist. Now we want to add it and start with settings. So I don't think the point that it doesn't do all it should do, it should do nothing.
.detach() is called when content on the page are remove. Content that might have IDs we need to remove. That's why it should be there and not on a piece of code that runs on every AJAX call, no matter if you remove nothing from the page.
This exists cause drupalSettings.ajax exist only when you have IDs added. If you don't have a form with #ajax attached to them it will not exist and it will throw a JavaScript error. Of course it could wrap everything in an if. Either works.
Comment #32
droplet commentedPatch #21 may not work in some conditions, it needed tests for sure. However if we put it after AjaxCommands, it's similar to `.detach()`.
The difference is:
ONE is called by Drupal AJAX system. At least you know you're doing some Ajax works now.
Another one can be executed by any another scripts at anytime. If we work with 3rd party scripts to detach DOM temporarily, any Drupal.detachBehaviors executions would remove all settings. I don't think it's what we expected. (also, running in non-scoped env)
IMO, both are not safe. that's why I raised the question in #21
Here're 2 problems:
1. We didn't merge the new settings to old settings correctly when doing Drupal Ajax (Insert & Remove). It's main reason caused settings growing up.
2. Zombie settings when the DOM is detached. ( Should we do auto-gc globally ? )
Both #21 & #27 fixing #2 in loose way.
Comment #33
nod_We're talking about drupalSettings.ajax getting out of hand but it's nothing next to how Drupal.ajax.instances get out of hand. sure it doesn't slow down attachbehaviors but it does eat up memory. Opening a few modals on the Views ui makes Drupal.ajax.instances go to 500+ objects very easily.
Turned the problem around and when a detach:unload happens, script checks all the Ajax object created and prune all objects for which the instance.element DOM element is not attached in the DOM anymore. Same things for settings, if one object is marked as pruned and it references settings in drupalSettings.ajax, remove the settings from there.
The graphs don't show it very clearly but the memory usage tends to be lower.
Comment #34
nod_There was an issue with views preview (at least) removal of settings was too agressive.
Now settings are cleaned up in the settings command and objects are removed in the detach behavior.
Comment #35
nod_interdiff is not very useful on two quick patches (just ignore #33) but here it is.
Comment #36
tic2000 commentedI tested this code. It does set to null all the correct instances, but I don't know how to check if gc gets them or not.
Comment #37
nod_Memory and GC are secondary concerns for this issue I'd say. having null is at least better than before.
Comment #38
nod_Comment #39
droplet commentedPatch #35 clean up the zombie furthermore. I only wonder why @nod_ chose a loose (less aggressive) way to clean up setting. (compare to #21)
Comment #40
nod_Mainly because it relies on setTimeout to work, I prefer not messing with the execution queue.
Comment #41
tic2000 commentedComment #42
catchLooks good to me. I pinged cottser in case he wants to take a look before it goes in but otherwise will try to get it in this week.
Comment #43
star-szrI'm not super familiar with all the Ajax guts but I think we can spell out GC here or just tweak this comment a bit. Leaving at RTBC.
Comment #44
catchJust realised this should really have jsdoc.
Something like 'Set this to null and allow garbage collection to reclaim the memory'.
Comment #45
nod_Interdiff failed me, only made doc changes.
The behavior doc might look strange, but it's been agreed upon, see http://read.theodoreb.net/drupal-jsapi/Drupal.behaviors.html for what it looks like.
Comment #46
wim leersThese are the new bits. i.e. this is the interdiff.
Only nit: -> . Can be fixed on commit.
Comment #49
catchCommitted/pushed to 8.1.x and cherry-picked to 8.0.x, thanks!
Comment #50
edurenye commentedI get an error in TMGMT due to this change.
Here 2 screenshots of the error, not sure why this happens, I'll provide a simplytest link.
Comment #51
nod_We checked Views UI but not the ajax behavior of a view itself.
Patch should fix it. Our docs do say element should be an HTMLElement, not a jquery object http://read.theodoreb.net/drupal-jsapi/Drupal.html#.ajax
Comment #52
edurenye commentedTested manually, it works fine now.
So setting it to RTBC.
Comment #53
edurenye commentedLet's fix it in a followup #2673824: Views JS passing wrong type of object to Drupal.ajax.
Comment #54
catchRolled back in 8.0.x - but committed the follow-up to 8.1.x
For this to go into 8.0.x I think we'd need more manual testing and/or scanning contrib code bases for similar errors.
Comment #55
edurenye commentedThis also caused this issue: #2705327: Failed to execute 'contains' on 'Node'
Comment #56
sam152 commentedNot sure if this is a duplicate of the above, but this also caused #2707631: Views AJAX pagination broken after the first page..
Comment #57
ayesh commentedAre there any plans for a port for D7?