apache / netbeans

Apache NetBeans
https://netbeans.apache.org/
Apache License 2.0
2.63k stars 841 forks source link

NbClipboard adjustments: Avoid blocking EDT, and ease retries #7668

Open eirikbakke opened 1 month ago

eirikbakke commented 1 month ago

Some improvements to the NbClipboard class, in three separate commits: 1) Adjust the retry logic from https://github.com/apache/netbeans/pull/6443 by applying an exponential backoff on the delays. I saw a large number of retries in the log on various occasions, as well as a case where Microsoft Excel complained about not being able to access the clipboard because NetBeans was hogging it (error message screenshot below).

image

(This commit I have been using in my working IDE and platform app since January 2024.)

2) Avoid blocking the Event Dispatch Thread during calls to systemClipboard.addFlavorListener()/removeFlavorListener(). This can happen because these methods are synchronized on the same object as the expensive systemClipboard.getContents() method. I once saw NetBeans Platform app hang for a long time in removeFlavorListener(), and tracked it down (with the VirtualVM profiler) to attempting to acquire the same lock as getContents(). The hang itself was caused by a different misbehaving application (Oracle VirtualBox), but I can imagine other cases where a more normal situation can also cause a multi-second hang.

Event dispatch thread (AWT-EventQueue):

image

Clipboard synchronizer thread concurrently holding the lock:

image

(This commit I have been using in my working IDE on Windows for only 5 days, but I haven't seen any problems with it so far. I'll be switching to a new MacBook laptop now, so my Windows setup won't see too much organic usage after this.)

3) Get rid of some dead code, after reviewing the historical commit from 2012 that left it unused. See the description of the third commit in this PR.

There are some long-standing intermittent bugs with Cut and Paste on Windows, which are either caused by, or have to be fixed with, changes in the NbClipboard class. The changes above were not specifically made to fix those bugs, but improving the code is probably a good thing in any case.

matthiasblaesing commented 3 weeks ago

Commits 58312a4585a1bece2c5849d84e4c4070c08379f2 and fb59d759e280f0a23d067e4867bf16873eb425f0 make sense to me.

The commit "Avoid calling Clipboard.add/removeFlavorListener from the Event Dispatch Thread [...]" gives me bad vibes 8c0e3d7188e023dfcfbbd79c07dc9b0f62f5f7083). The one mantra in Swing I learned was: You interact with Swing/AWT only from the EDT, unless it is explicitly documented, that you can deviate from this rule. I don't see that exception in the Clipboard class. I can imagine implementations that are prone to races.

eirikbakke commented 3 weeks ago

@matthiasblaesing Yeah, it's not officially documented in the Javadoc for the java.awt.datatransfer.Clipboard class, but reading through its source code, it's clear that it is intended to be thread-safe, and NetBeans already assumes this to be the case (see the GetContents and SetContents tasks, which are run on a separate RequestProcessor thread). The class does I/O operations that may block for several seconds, so it can't be used purely on the event dispatch thread without regularly locking up the UI.

In the Clipboard.addFlavorListener/removeFlavorListener case, you can call either method from any thread, but the actual events will be delivered on the Event Dispatch Thread. The fact that addFlavorListener/removeFlavorListener can block for several seconds (due to lock contention with the I/O-heavy getContent operation) was almost certainly unintentional, but calling them off-thread avoids the problem of locking up the UI.

Reviewing the logic again, I can try to justify why the proposed code is safe...

matthiasblaesing commented 2 weeks ago

@eirikbakke I'm aware, that there is already a threading violation in NbClipboard. My concern is, that that is the reason for #3962. And my concern is also not what happens on the NetBeans side of the Clipboard, but what happens on the way from NetBeans layer, through AWT to native. In the end setContent and getContent at some point reach the native layer. That native layer might make assumptions about which threads it might get calls from. AWTs "every allowed interaction comes from the EDT" makes it easy to reason about this from the native side, it just breaks down when people ignore the mantra.

I saw a window message loop inside AWT and if I remember correctly I diagnosed a problem in AWT once that related to the threading assumption of the COM calls inside the AWT.

eirikbakke commented 2 weeks ago

@matthiasblaesing It's quite possible (likely, even) that https://github.com/apache/netbeans/issues/3962 has race conditions involved. Someone would have to spend a week(end) finding the root cause of it before we could figure the best way to solve it, though. For me it was never worth it, because the previous patches, or just other environmental conditions, made the problem rare and easy to work around.

Though often I find that fixing smaller understandable bugs, like the blocking removeFlavorListener() here, sometimes magically ends up fixing bigger hard-to-reproduce bugs.

For the systemClipboard.addFlavorListener()/removeFlavorListener() calls it is (somewhat) easy to reason about concurrency behavior by reading the source code of java.awt.datatransfer.Clipboard class. For getContents/setContents things go much deeper, as you mention.

Since I have now switched permanently from Windows to MacOS on my working machine, it is hard for me to properly test further changes to this PR, as these clipboard bugs usually come only after several days of active use. If preferred, I could drop the controversial "Avoid calling Clipboard.add/removeFlavorListener from the Event Dispatch Thread" commit and leave the two others.