Skip to content

OwnPaste: read "delegates" without blocking on pending updates - #9772

Open
mbien wants to merge 1 commit into
apache:masterfrom
mbien:nb-paste-edt
Open

mbien wants to merge 1 commit into
apache:masterfrom
mbien:nb-paste-edt

Conversation

@mbien

@mbien mbien commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

this is plan B. If debouncing the lookup updates isn't feasible (#9771) we have to make the event consumers non-blocking. This is trying to avoid to block on clipboard reads during selection change until it is required. This change alone fixes the performance problem (UI freeze due to queue back pressure can't be provoked anymore with the method described in #9771).

However: nobody wants to touch clipboard code given the history, so I wanted to avoid this originally.

blocking IO path looked like:

EDT  ──waitFinished(100)──▶  "Explorer Actions" RP task
                              └─ updateTrans() → NbClipboard.getContents()
                                   ──waitFinished(1000)──▶  "NBClipboard" RP task
                                                             └─ XSelection.getData
                                                                  (X-server round-trips, AWT lock)

a quick way to simulate slow OS clipboard IO is to provoke the worst case wait:

diff --git a/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java b/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java
index 478230c..38efbca 100644
--- a/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java
+++ b/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java
@@ -855,6 +855,7 @@
         public void update() {
             if (EventQueue.isDispatchThread()) {
                 try {
+                    Thread.sleep(100);
                     timer.waitFinished(100);
                 } catch (InterruptedException ex) {
                     LOG.log(Level.FINE, null, ex);

(and now try using the tree with this change active)

commit:

  • getValue("delegates") called ActionStateUpdater.update(), which on the EDT waits up to 100 ms for the "Explorer Actions" task; the paste menu model reads "delegates" on every action enabled-state flip, so selection bursts serialized the EDT behind the updater, which was itself waiting on X11 system clipboard reads
  • make "delegates" a non-blocking read of the current paste types and move the flush to actionPerformed, so only the user-initiated paste path waits for up-to-date types
  • drop stale paste types when an update is scheduled, so the global PasteAction (which derives its enabled state from the types, not the action flag) is corrected by the force-disable event instead of staying enabled with stale entries
  • 30 s wall profile while holding an arrow key: EDT busy time drops from 69% to 24%

before/after flame graphs (async-profiler):

Details

before:
flame-edt-prefix

after:
flame-edt-postfix

Assisted-by: qwen3.8-27b

@mbien mbien added this to the NB33 milestone Oct 31, 2026
@mbien
mbien requested a review from neilcsmith-net October 31, 2026 20:21
@mbien mbien added performance Editor Platform [ci] enable platform tests (platform/*) ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) UI User Interface labels Oct 31, 2026
- getValue("delegates") called ActionStateUpdater.update(), which on
  the EDT waits up to 100 ms for the "Explorer Actions" task; the
  paste menu model reads "delegates" on every action enabled-state
  flip, so selection bursts serialized the EDT behind the updater,
  which was itself waiting on X11 system clipboard reads
- make "delegates" a non-blocking read of the current paste types
  and move the flush to actionPerformed, so only the user-initiated
  paste path waits for up-to-date types
- drop stale paste types when an update is scheduled, so the
  global PasteAction (which derives its enabled state from the
  types, not the action flag) is corrected by the force-disable
  event instead of staying enabled with stale entries
- 30 s wall profile while holding an arrow key: EDT busy time drops
  from 69% to 24%

Assisted-by: qwen3.8-27b

@jtulach jtulach left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • too continue on previous note...
  • it is painful to look at the old Java code
    • its 2026 and we have CompletableFuture & co.
    • I would probably write the code differently these days
  • it is hard to suggest what to do without rewriting the code significantly...
  • ... maybe detect arrow down/up and then stop slowing down the dispatch thread:
# This patch file was generated by NetBeans IDE
# It uses platform neutral UTF-8 encoding and \n newlines.
--- a/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java
+++ b/platform/openide.explorer/src/org/netbeans/modules/openide/explorer/ExplorerActionsImpl.java
@@ -787,6 +787,7 @@
         private final PropertyChangeListener weakL;
         private FlavorListener flavL;
         private Transferable trans;
+        private transient boolean tooSlowAwait;
 
         ActionStateUpdater(ExplorerManager m) {
             timer = RP.create(this);
@@ -855,8 +856,14 @@
         public void update() {
             if (EventQueue.isDispatchThread()) {
                 try {
+                    if (!tooSlowAwait) {
                     timer.waitFinished(100);
+                    }
                 } catch (InterruptedException ex) {
+                    // when the wait for an update is known to time out
+                    // then make a note and never wait from AWT dispatch 
+                    // thread again. Until an update is rescheduled
+                    tooSlowAwait = true;
                     LOG.log(Level.FINE, null, ex);
                 }
             } else {
@@ -871,6 +878,7 @@
             deleteActionPerformerNoConfirm.toEnabled(false);
             pasteActionPerformer.toEnabled(false);
             EventQueue.invokeLater(this);
+            tooSlowAwait = false;
             timer.schedule(0);
         }

@Override
public Object getValue(String s) {
if ("delegates".equals(s)) { // NOI18N
String prev = "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Hard to foresee the consequences of this "await check" removal.
  • It is good to know it fixes the slow clipboard problem
  • but it can break anything anywhere...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep it would need some good manual testing. (I am using it atm so i am doing my part)

But code inspection showed that NB itself should do fine. I did some initial digging but we should do more:

Path Before After Risk
Paste menu content fresh (blocked until update) current field, self-correcting on next state change menu one RP-tick stale if opened within ms of a selection/clipboard change — imperceptible in practice
Paste via OwnPaste.actionPerformed (direct) no flush, used current field flush added — strictly fresher none (one-shot, user-initiated)
Paste via global PasteAction (Ctrl+V routing, PasteAction.actionPerformed line 159) fresh (blocked) reads field without flush; if stale value is null it routes to OwnPaste.actionPerformed which flushes only real window: paste within ~ms of a selection change while old non-null types exist → pastes old types. A human can't hit this; a macro could
Copy/cut/delete, drag&drop, flavor listener, setPasteTypes — untouched none

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding the change request:

                 try {
+                    if (!tooSlowAwait) {
                     timer.waitFinished(100);
+                    }
                 } catch (InterruptedException ex) {
+                    // when the wait for an update is known to time out
+                    // then make a note and never wait from AWT dispatch 
+                    // thread again. Until an update is rescheduled
+                    tooSlowAwait = true;
                     LOG.log(Level.FINE, null, ex);
                 }

I think this alone wouldn't work since task impl calls Object.wait(mills) and InterruptedException is only thrown when the wait is actually interrupted by something, not on timeout.

Also from a different point of view:

  • the existing code already allows the result to be inaccurate - that is why its wait-with-timout and not indefinite wait
  • that is why I believe we actually have a chance to make this completely non-blocking (cleanest approach IMO)

pasteActionPerformer.toEnabled(false);
// paste types are unknown until the update runs, so drop the
// stale ones as well; setPasteTypes(null) also force-disables
pasteActionPerformer.setPasteTypes(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • immediately setting the state to "unknown" is correct, but then:
  • some code must wait for the state to become known...
    • for example OwnPaste.actionPerformed!?
  • I don't see such a change

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actionPerformed has a blocking wait, no?

            // Make sure the paste types are up to date before pasting. This
            // may briefly wait for a pending update, but only on the
            // user-initiated paste path, not on every selection change.
            ActionStateUpdater asu = actionStateUpdater;
            if (asu != null) {
                asu.update();
            }

and update() calls timer.waitFinished(100)

as the comment says, blocking there is far less critical than during simple selection change since this would be actually the action activation. A small delay there is not noticeable + there is no event burst problem.

@neilcsmith-net

Copy link
Copy Markdown
Member

this is plan B. If debouncing the lookup updates isn't feasible (#9771) we have to make the event consumers non-blocking. This is trying to avoid to block on clipboard reads during selection change until it is required. This change alone fixes the performance problem (UI freeze due to queue back pressure can't be provoked anymore with the method described in #9771).

However: nobody wants to touch clipboard code given the history, so I wanted to avoid this originally.

While I agree with the principle of ensuring consumers are non-blocking, I also understand the concern about touching clipboard code - too many bad memories.

I'm not sure debouncing on the producer side is infeasible. My suggestion at #9771 (comment) was to also consider the lookup as a consumer, and look before the stage you were looking at, in the UI layer. eg. debouncing rapid selection changes from the tree or list before the selected node change is passed to the explorer manager? With something like key repeat all the EM really needs to capture is the first and last change?

@mbien mbien removed this from the NB33 milestone Nov 1, 2026
@mbien

mbien commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

yep we could try approaching this from other points. But I think the "hold down arrow key" showed that there is a problem in the selection event handler side specifically.

The lookup debouncing avoided fixing the issue by trying to call it less frequently. Debouncing even further up (at UI level) would be similar (although it would break fewer tests since we don't have so many UI tests). But: the issue is still there sleeping (pun intended): The blocking IO call on the consumer side remains.

Not to mention we could do both: debounce somewhere + don't do IO in a handler. I'll leave this open a while maybe we get better ideas - there is no hurry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) Editor performance Platform [ci] enable platform tests (platform/*) UI User Interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants