flow: Infinite Lambda Chaining in PendingJavaScriptInvocation (Memory Leak)

Description of the bug

Consider the minimal reproducible example below.

Once a button is attached a JS is invoked via Button#initDisableOnClick. Since the button b is not visible, that pending JS invocation will be retained internally. For those invisible components/nodes the pending JS is retained but now with every interaction with the view you can get an infinite lambda chain.

Each time you click on button a, during UidlWriter#createUidl the UIInternals#dumpPendingJavaScriptInvocations method is executed, which at the end is calling UIInternals#registerDetachListenerForPendingInvocation for each retained pending JS invocation. Finally, invocation.then(callback, callback) is being called which then calls

this.successHandler = combineHandlers(this.successHandler, successHandler);

This is called because

  • Initially, sentToBrowser is false because this invocation has not been sent yet since its node was not visible yet.
  • and the invocation is not canceled
  • and the provided successHandler from UIInternals is not null

The provided successHandler parameter is always the same:

SerializableConsumer callback = unused -> listener.onInvocationCompleted(invocation)

Now, during the first call the PendingJavaScriptInvocation#successHandler is null, so combineHandlers will just return the provided not null successHandler and store that in the field.

But, each time you click now on button a in the browser, next combineHandlers will have both not null, the field successHandler as well as the param successHandler. This leads to combining both to a new lambda and storing that in the field successHandler. Next time you click again and again, so you get an endless chaining of lambdas.

Screenshot 2023-06-22 at 1 09 47 PM

Remember that those chained lambda callbacks in the end are always the same ones provided by the calling method UIInternals#registerDetachListenerForPendingInvocation. The chaining would make sense if the callbacks were different ones, but in this case it’s always the same callbacks each effectively doing a onInvocationCompleted() once called.

Assuming that an app has components that are initialized or implemented like in this minimal example, eventually this situation will make the app end up in an OutOfMemoryError, the more invisible components with pending JS there are the faster this will be reached.

Expected behavior

There should be no memory leak in PendingJavaScriptInvocation via potential endless chaining of lambdas.

Minimal reproducible example

@Route("")
public class View extends Div {

  public View() {
    Button a = new Button("test");
    Button b = new Button("invisible");
    b.setVisible(false);
    add(a, b);
  }
}

Each time you click on button a, the PendingJavaScriptInvocation#successHandler gets a new lambda chain longer than the previous one.

Versions

  • Vaadin / Flow version: 14.10.1
  • Java version: 17.0.6
  • OS version: macOS

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 19 (5 by maintainers)

Commits related to this issue

Most upvoted comments

The fix has been released in Flow 23.3.14, included in Vaadin platform since 23.3.15. For some reason, it is not mentioned in the release notes

@muhammetaltindal thank you for the detailed issue and for the insights. We will start working on this shortly.