feat: warn about JavaScript invocations that are never delivered to the client - #25252
Open
totally-not-ai[bot] wants to merge 6 commits into
Open
feat: warn about JavaScript invocations that are never delivered to the client#25252totally-not-ai[bot] wants to merge 6 commits into
totally-not-ai[bot] wants to merge 6 commits into
Conversation
UIInternals.setTitle built its own JavaScriptInvocation and PendingJavaScriptInvocation and queued them directly, duplicating what Page.executeJs already does with the same owner node. Delegate to Page.executeJs instead, so that PendingJavaScriptInvocation instances are only created in Element and Page. This keeps the behavior unchanged and leaves a single place to hook bookkeeping of scheduled invocations into.
Invocations scheduled with executeJs are retained in memory until they are sent to the browser, which means that an application that keeps scheduling invocations that are never delivered runs out of memory. This is invisible to the developer until a heap dump is analyzed, since neither the invocations parked in the before-client-response queue of a state node nor the ones waiting in UIInternals are reported anywhere. Count the invocations that have been scheduled for a state node without being sent, and log a warning when the count reaches 1000 for a single owner, repeating it whenever the count grows tenfold. The warning names the owner component and the most recently scheduled expression, tells whether the owner is detached or invisible and whether the UI has an open push connection, and describes how to schedule updates where only the latest value matters, either by canceling the previous PendingJavaScriptResult or by setting an element property instead. Debug logging adds the call site. The threshold can be changed with the new pendingJavaScriptInvocationsWarningThreshold configuration property, and 0 disables the warning. Counts are only inspected at every 100th invocation to keep the scheduling path cheap, so the configured threshold is in practice rounded up to a multiple of 100.
A background task that updates a UI at a regular interval has no supported way of noticing that its updates are not reaching the client and only accumulate in memory. Checking the push connection does not cover UIs that rely on polling, and the invocations are usually scheduled indirectly, for instance by a Binder, so the application does not hold the PendingJavaScriptResult that would tell whether anything was sent. Record the time when the queue of pending updates is purged into a response for the client and expose it as UI.getLastUpdateSentTimestamp(), so that a background task can compare it against the current time and stop scheduling updates while nothing is being written to the client. The timestamp is initialized when the UI is created, so a UI that has never sent anything reports its creation time instead of zero. The warning about undelivered invocations now points to this method as the way for an application to avoid piling up unsent updates.
Contributor
A running application has far more state nodes than UIs, so the field that counted the invocations scheduled for a node multiplied into a noticeable amount of resident memory even for the vast majority of nodes that never schedule a single JavaScript invocation. Keep the count in UIInternals instead, where a single field per UI is insignificant, and let the pending invocation remember which UI counted it so that it decrements the same counter when it is sent or canceled. The counter is resolved from the owner node when it is attached, and otherwise from the current UI, which keeps invocations scheduled for a detached component counted in the UI whose code scheduled them. Since the threshold is now reached per UI rather than per component, the warning reports the number for the UI and names the component the most recently scheduled invocation belongs to. Diffuse accumulation across many components is now covered as well. The counter accessors are package private, so this also removes the public methods that were added to StateNode.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


JavaScript invocations scheduled from the server are retained in memory until they are written into a response for the client, so an application that keeps scheduling invocations that are never delivered runs out of memory. Nothing reports this today: the developer sees it for the first time as hundreds of thousands of
PendingJavaScriptInvocationandStateTree$BeforeClientResponseEntryinstances in a heap dump, with no indication of which component schedules them or why they are stuck. See this forum thread for how hard this is to diagnose from the outside, and #17286 for the related case of invocations retained for invisible components.Invocations pile up in three different places, which is why counting only the queue in
UIInternalswould not have covered the reported case:Element.executeJsparks the invocation in abeforeClientResponseentry on its state node, and those entries are only flushed while a UIDL response is being written. Without a response, the invocations never even reachUIInternals.executeJscall on a detached node registers an attach listener that holds the invocation until the node is attached again.UIInternals.pendingJsInvocationsgrows throughPage.executeJs, and invocations owned by an invisible component are deliberately re-queued on every purge.What changed
Counting at the source. The invocations that have been scheduled for a UI without being sent are counted in
UIInternals, hooked into thePendingJavaScriptInvocationlifecycle: the constructor counts one, andsetSentToBrowser()/cancelExecution()uncount it. Counting happens when an invocation is scheduled rather than when it enters the UI queue, so it covers all three cases above, including the invocations that never reach that queue. Each invocation remembers which UI counted it, so the same counter is decremented later even if its owner has been detached meanwhile. The counter is deliberately not kept per state node: a running application has far more nodes than UIs, so even one field per node multiplies into a significant amount of resident memory for nodes that never schedule any JavaScript.A warning that identifies the culprit.
PendingJavaScriptInvocationUtillogs a warning when a UI reaches 1000 undelivered invocations, and repeats it only when the count grows tenfold, so a leaking application logs a handful of lines instead of thousands. The message names the component the most recently scheduled invocation belongs to (plus its creation location when component tracking is enabled), its expression, and whether that owner is detached, invisible, or in a UI with no open push connection. Debug logging adds theexecuteJscall site. Example, from a UI with push disabled that keeps updating a field:Advice for the common cause. The message explains what to do when only the latest value is relevant for the client, for example a progress value or the current time: keep the
PendingJavaScriptResultand cancel it before scheduling the next update (which is what Flow itself does for the page title), or set an element property instead, since only the last value of a property is sent.UI.getLastUpdateSentTimestamp()exposes when the pending updates of a UI were last purged into a response for the client, stamped inUIInternals.dumpPendingJavaScriptInvocations(). A background task that updates a UI at an interval can compare it against the current time and stop scheduling updates while nothing is reaching the client — which works for polling as well as for push, and does not require the application to hold thePendingJavaScriptResultof updates it never scheduled itself (aBinderreading a bean, for instance). It is initialized when the UI is created, so a UI that has never sent anything reports its creation time rather than 0. The javadoc is explicit that this says the updates were written towards the client, not that the client received them; the warning message points to the method as well.Configuration. The new
pendingJavaScriptInvocationsWarningThresholdinit parameter changes the threshold, and 0 disables the warning. Counts are only inspected at every 100th invocation to keep the scheduling path to a single modulo, so a configured threshold is in practice rounded up to a multiple of 100.API
Added:
UI.getLastUpdateSentTimestamp(),UIInternals.getLastUpdateSentTimestamp()andInitParameters.PENDING_JAVASCRIPT_INVOCATIONS_WARNING_THRESHOLD. Nothing removed or changed.PendingJavaScriptInvocationUtiland the counter accessors onUIInternalsare package private, andStateNodeis untouched. The added memory is oneintper UI, plus one reference on eachPendingJavaScriptInvocationthat is waiting to be sent.Verification
mvn -pl flow-server testpasses (5056 tests, 12 of them new). The new tests cover the warning tiers (first warning at the threshold, then only tenfold, and 0 disabling it), the counter over the scheduled → sent and scheduled → canceled paths including a double cancel, an invocation for a detached owner being counted in the current UI and one with no UI at all not being counted, the contents of the message for a detached, an invisible and a connection-less owner, truncation of long expressions to a single line,executeJson a detached element, and the purge timestamp being updated by a purge but not by scheduling an invocation. The end-to-end behaviour was checked separately by scheduling 1000executeJscalls in a UI without push and confirming that exactly one warning is logged, with the text quoted above.