Repository navigation
Conversation
- 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
There was a problem hiding this comment.
- 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
- its 2026 and we have
- 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 = ""; |
There was a problem hiding this comment.
- 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...
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
- immediately setting the state to "unknown" is correct, but then:
- some code must wait for the state to become known...
- for example
OwnPaste.actionPerformed!?
- for example
- I don't see such a change
There was a problem hiding this comment.
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.
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? |
|
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. |
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:
a quick way to simulate slow OS clipboard IO is to provoke the worst case wait:
(and now try using the tree with this change active)
commit:
getValue("delegates")calledActionStateUpdater.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 readsactionPerformed, so only the user-initiated paste path waits for up-to-date typesPasteAction(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 entriesbefore/after flame graphs (async-profiler):
Details
before:

after:

Assisted-by: qwen3.8-27b