RangeError with 50 PolymerTemplates and Buttons
See original GitHub issueI 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:
- Created 4 years ago
- Comments:11 (5 by maintainers)
Top GitHub Comments
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 explicitflush()
is triggered if the element is processed from the asynchronousready
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 aflush()
which picks the first child flush listener from the queue. This triggers a (non-async) Polymerready
event for the child which in turn leads to a newflush()
. 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 eachflush()
on the stack will see an empty queue and thus return.I see three alternative ways out of this problem:
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.flush()
that is a no-op if run while anotherflush()
is already ongoing and use that variant for this particular case. This adds some complexity inReactive
where it “belongs”, but it would put a burden on each caller offlush()
to know which variant to use.flush()
to always bail out in case aflush()
is already ongoing. This seems safe based on my quick analysis of all (non-test) cases that are callingflush()
, but there’s no good way to be 100% certain.@denis-anisimov: What do you think?
I don’t completely understand this code but the basic recursive call structure is:
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.