Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NbClipboard adjustments: Ease retries, remove dead code #7668

Merged
merged 2 commits into from
Sep 27, 2024

Conversation

eirikbakke
Copy link
Contributor

Some improvements to the NbClipboard class, in three separate commits:

  1. Adjust the retry logic from Retry fetching clipboard contents when it initially fails #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.)

  1. 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.)

  1. 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.

@eirikbakke eirikbakke added Platform [ci] enable platform tests (platform/*) UI User Interface os:windows labels Aug 12, 2024
@matthiasblaesing
Copy link
Contributor

Commits 58312a4 and fb59d75 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
Copy link
Contributor Author

eirikbakke commented Aug 26, 2024

@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...

  • A race condition bug around the systemClipboard.addFlavorListener call is avoided by ensuring that fireChange() is called only after the systemClipboard.addFlavorListener has returned (to ensure that we don't "miss" a fireChange() call), and by confirming that fireChange() does no harm if called an extra time (it looks fine).
  • A race condition bug around the systemClipboard.removeFlavorListener call is avoided by the fact that the RequestProcessor RP has a throughput of one (thus adding/removing listeners will happen in order), and the fact that extra calls to flavorsChanged do no harm.
  • Looking at flavorsChanged, I see that it skips the call to fireChange() if anyWindowIsActivated == false. I confirmed that whenever anyWindowIsActivated is set to true (in the handler for WINDOW_ACTIVATED), this is followed by a new call to fireChange (via scheduleGetFromSystemClipboard(true) and GetContents.run).

@matthiasblaesing
Copy link
Contributor

@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
Copy link
Contributor Author

@matthiasblaesing It's quite possible (likely, even) that #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.

@matthiasblaesing
Copy link
Contributor

If preferred, I could drop the controversial "Avoid calling Clipboard.add/removeFlavorListener from the Event Dispatch Thread" commit and leave the two others.

I think this would be a good idea. Thank you.

@eirikbakke eirikbakke changed the title NbClipboard adjustments: Avoid blocking EDT, and ease retries NbClipboard adjustments: Ease retries, remove dead code Sep 26, 2024
@eirikbakke
Copy link
Contributor Author

I removed the "Avoid calling Clipboard.add/removeFlavorListener from the Event Dispatch Thread" commit and left the other two.

@eirikbakke
Copy link
Contributor Author

Also added a comment that MAX_RETRIES is just a fail-safe, and that the loop will stop earlier normally. (I left it at 10 rather than 7 to avoid having to justify the exact number with a long irrelevant comment.)

The fields lastWindowActivated, lastWindowDeactivated, and lastWindowDeactivatedSource, and the related conditions were no longer in actual use; this can be seen by reading through the code.

It seems the condition that once used lastWindowActivated was at some point replaced by a call to Task.waitFinished. So there wasn't a mistake; just some unused code left around. See the relevant commit from 2012: eirikbakke/netbeans-releases@6de619c#diff-69d67e2e5b062ae995069c62294958e6e24a42c599cc6684c47f89b086c20278
…reating a lot of logging messages, and clipboard access is a heavy operation that can sometimes interfere with other applications.
Copy link
Contributor

@matthiasblaesing matthiasblaesing left a comment

Choose a reason for hiding this comment

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

Looks sane to me. Thank you.

@eirikbakke eirikbakke merged commit 534340c into apache:master Sep 27, 2024
32 checks passed
@eirikbakke
Copy link
Contributor Author

Thanks! Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
os:windows Platform [ci] enable platform tests (platform/*) UI User Interface
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants