question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RangeError with 50 PolymerTemplates and Buttons

See original GitHub issue

I have a page that displays 50 PolymerTemplates and each template has a single button. Using long polling push, a client side JavaScript error is raised in Chrome when the content is rebuilt. It doesn’t look like the error occurs in FireFox, only Chrome.

This may be a client component specific issue, but the issue only seems to occur if I link the Button to the Java code via the @Id attribute so it may be more flow related than a specific client side component.

This issue causes a scalability problem for us because we display a dashboard of recent events, each with a details and acknowledge button. In rare cases, that recent event list can grow to more than 50 items. We can work around it by supporting paging or displaying a subset of the items, but 50 templates/buttons doesn’t seem to extreme for a single page.

Test case with a simple view and panel is attached. Bug 5806 Test Case.zip

Config info:

  • Vaadin version: 13.0.7
  • Chrome version: 74.0.3729.169
  • Java version: openjdk version “11.0.3” 2019-04-16
  • OS version: MacOS 10.14.4
Uncaught RangeError: Maximum call stack size exceeded.
    at vB (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:404)
    at Bv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:760)
    at ew.iw [as yb] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at Ju (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:714)
    at nv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:941)
    at Fv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:902)
    at Gx.Hx [as W] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at eA (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:866)
    at pv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:976)
    at Zw.$w [as J] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at Function.yl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at d (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:637)
    at Set.forEach (<anonymous>)
    at bl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:747)
    at HTMLElement.s.ready (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at HTMLElement._enableProperties (properties-changed.html:321)
    at HTMLElement.connectedCallback (properties-mixin.html:219)
    at HTMLElement.connectedCallback (element-mixin.html:595)
    at nv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:941)
    at Fv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:902)
    at Gx.Hx [as W] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at eA (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:866)
    at pv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:976)
    at Zw.$w [as J] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at Function.yl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at d (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:637)
    at Set.forEach (<anonymous>)
    at bl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:747)
    at HTMLElement.s.ready (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at HTMLElement._enableProperties (properties-changed.html:321)
    at HTMLElement.connectedCallback (properties-mixin.html:219)
    at HTMLElement.connectedCallback (element-mixin.html:595)
    at nv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:941)
    at Fv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:902)
    at Gx.Hx [as W] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at eA (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:866)
    at pv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:976)
    at Zw.$w [as J] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at Function.yl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at d (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:637)
    at Set.forEach (<anonymous>)
    at bl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:747)
    at HTMLElement.s.ready (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at HTMLElement._enableProperties (properties-changed.html:321)
    at HTMLElement.connectedCallback (properties-mixin.html:219)
    at HTMLElement.connectedCallback (element-mixin.html:595)
    at nv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:941)
    at Fv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:902)
    at Gx.Hx [as W] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at eA (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:866)
    at pv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:976)
    at Zw.$w [as J] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at Function.yl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at d (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:637)
    at Set.forEach (<anonymous>)
    at bl (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:747)
    at HTMLElement.s.ready (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at HTMLElement._enableProperties (properties-changed.html:321)
    at HTMLElement.connectedCallback (properties-mixin.html:219)
    at HTMLElement.connectedCallback (element-mixin.html:595)
    at nv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:941)
    at Fv (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:902)
    at Gx.Hx [as W] (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:983)
    at eA (client-3B1D64116B46DEBA7ADE32AB75416693.cache.js:866)

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:11 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
Legiothcommented, Aug 29, 2019

Thanks for extracting those details. Based on those, the issue is really clear for anyone of the ~3 persons in the world who actually know how those parts of the code are supposed to work.

The client-side processing is really roughly split up in two separate phases: we first update the internal representation of the state, and then we update the DOM to match the updated state. The first phase adds flush listeners which are then run during the second phase.

Processing is split up in this way because in some cases a DOM update is dependent on multiple different state updates and it will either fail or do redundant work if the DOM update is done before all state changes have been applied.

In the case of @Id elements, there’s one extra asynchronous step because Polymer must first create the target element and only then can we find it and apply server-side changes to it. This means that the updates for the @Id element cannot be applied during the “master” flush() that happens when the state has been fully updated based on changes from the server. For this reason, an explicit flush() is triggered if the element is processed from the asynchronous ready event from Polymer.

In this case with many @Id elements added as children of another @Id element, processing the parent element causes one flush listener to be added to the queue for each child. It then goes on to do a flush() which picks the first child flush listener from the queue. This triggers a (non-async) Polymer ready event for the child which in turn leads to a new flush(). This flush will pull the flush listener of the second child from the queue and execute it, and so the recursion goes on. If the stack doesn’t blow, all child flush listeners will eventually be purged from the queue and each flush() on the stack will see an empty queue and thus return.

I see three alternative ways out of this problem:

  1. Schedule this particular flush() in a microtask instead of running it inline. This technically simple and have a very small risk of side effects. On the other hand, it would add additional complexity and asynchronosity to this case which is already too complex and too asynchronous.
  2. Add a variant of flush() that is a no-op if run while another flush() is already ongoing and use that variant for this particular case. This adds some complexity in Reactive where it “belongs”, but it would put a burden on each caller of flush() to know which variant to use.
  3. Change the current flush() to always bail out in case a flush() is already ongoing. This seems safe based on my quick analysis of all (non-test) cases that are calling flush(), but there’s no good way to be 100% certain.

@denis-anisimov: What do you think?

1reaction
mpilonecommented, Aug 28, 2019

I don’t completely understand this code but the basic recursive call structure is:

  1. Reactive.flush:97
  2. SimpleElementBindingStrategy.handleChildrenSplice:1012
  3. SimpleElementBindingStrategy.addChildren:1055
  4. DomAPI.insertBefore
  5. SimpleElementBindingStrategy.ready:304
  6. PolymerUtils.fireReadyEvent:583
  7. SimpleElementBindingStrategy.addVirtualChild:839
  8. Reactive.flush:97

It looks like a ready listener is added to PolymerUtils to call appendVirtualChild at SimpleElementBindingStrategy.java:887 with reactivePhase=false. This causes the Reactive.flush method to be called recursively at line 910 when the listener is fired. It seems like that is going to greatly reduce scalability (and potentially performance) because the call stack will be directly related to the number of virtual children elements on the page.

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found