Skip to content

Move Splash initialization to EDT and make it no longer modal#9303

Merged
mbien merged 1 commit intoapache:masterfrom
mbien:edt-splash
Apr 1, 2026
Merged

Move Splash initialization to EDT and make it no longer modal#9303
mbien merged 1 commit intoapache:masterfrom
mbien:edt-splash

Conversation

@mbien
Copy link
Copy Markdown
Member

@mbien mbien commented Mar 28, 2026

  • about 100ms faster startup since some of the swing initialization moved off the main thread (Main#start method execution time measured) and technically more correct anyway
  • use JFrame to get double buffering (no progress bar flicker during repaint) and we can use the opportunity to initialize swing asynchronously while NB is busy bootstrapping.
  • other change: splash window is no longer modal
  • code modernization

most changes come from the fact that the model needed to be extracted from UI code so that it can be updated from any thread while the UI paints on EDT.

have to test the dev build on other systems, esp how it interacts with the Java 6 --splash JVM feature (doesn't seem to be used from the linux launcher). I suppose that one would be still modal before its replaced but its hopefully not noticeable. edit: tested it in #9303 (comment)

more startup optimizations to come later

closes #8557

@mbien mbien added this to the NB30 milestone Mar 28, 2026
@mbien mbien added Code cleanup Label for cleanup done on the Netbeans IDE performance Platform [ci] enable platform tests (platform/*) UI User Interface ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) labels Mar 28, 2026
@neilcsmith-net
Copy link
Copy Markdown
Member

neilcsmith-net commented Mar 28, 2026

+1 to moving the Swing initialization into the right place.

From what I recall, the JVM --splash feature is never used any more and has been disabled since #1246

Since that PR we really should have some code in Splash to set the WM_CLASS on it. The code at https://github.com/apache/netbeans/blob/master/platform/core.windows/src/org/netbeans/core/windows/view/ui/MainWindow.java#L127 comes too late. We have a workaround in the NetBeans installer builds to work around this with the desktop file name, but it's not ideal. I did look at this once, but getting the right value is difficult - maybe just need a different branding token for it. Not saying you have to look at that in the context of this PR either! Just making a note.

I assume by "no longer modal" you mean the window type? What changes does that introduce across OS? Curious why it was felt necessary originally?

@mbien mbien marked this pull request as ready for review March 28, 2026 17:53
@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 28, 2026

if the modal change is controversial I can revert it. This is the first of a series of startup optimization PRs - I started with the simple changes. The modality issue was just a drive by change since I had to test it anyway but not the goal.

edit: one aspect where this is useful though is while debugging. If you have a breakpoint somewhere in the initialization phase the modal splash is often in the way when wanting to see the debugger.

@neilcsmith-net
Copy link
Copy Markdown
Member

No issue with the modal revert in my opinion, even if it does bring that old issue back in fixing another one.

I'd still like @eirikbakke opinion as he made the changes in #1246 I'm also curious looking at the docs for SplashScreen to revisit why using that support was not enough for hi-dpi.

Once that's resolved I'll see if I can fix that WM_CLASS issue in here too, as the workaround in the installers is a little fragile.

@eirikbakke
Copy link
Copy Markdown
Contributor

Thanks for this!

Reading over the Splash class, it does not seem to be very thread-safe, nor does it document which thread may access each method. My impression is that the Splash class is externally accessed from the "main" thread, i.e. thread on which main(String args[]) was originally called. Is that right?

For example, SplashComponent (comp) is constructed on the main thread, though it really should be constructed on the EDT. After this PR, it is added to the Frame (frame) on the EDT. In another example, SplashPainter.barLength is updated on the main thread and read on the EDT, without synchronization or "volatile" anywhere.

Perhaps we'd need to clean up the rest of the threading issues in this class, before we can safely move any of the code between threads? It's possible that some of this worked only by accident before.

On the modality thing: Sure, sounds good to make it non-modal. I tested it on MacOS and didn't see any problems with it.

On the USE_LAUNCHER_SPLASH = false thing: I could not get this working with HiDPI on Windows in the past. It's also quite a lot of complexity spent on getting the splash screen to show just a tiny bit earlier in the startup cycle. I don't think it's worth trying to reintroduce it. (On the contrary, it might be a good cleanup to just get rid of the related code, e.g. the part that stores the png of the splash screen to make it available for the splash parameter on next startup etc.)

On WM_CLASS: It's good to have this set properly, yes! Is your concern that it is not being set on the splash screen frame, only on the eventual main window?

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 30, 2026

For example, SplashComponent (comp) is constructed on the main thread, though it really should be constructed on the EDT

overlooked that one. will try to move that somewhere else too after I entangled the spaghetti code

I just realized that showAboutDialog() isn't used anywhere so the inner class I renamed is dead code.

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 30, 2026

this is a mess. The test only worked because it completed before the EDT ran.
max steps are hardcoded in the init method. Test expects that the set values are used.

// 100 is allocated for module system that will adjust this when number
// of existing modules is known
maxSteps = 140;

@mbien mbien force-pushed the edt-splash branch 2 times, most recently from 99edbab to e96a034 Compare March 30, 2026 04:44
@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 30, 2026

Tried to make this work without rewriting everything. Also switched to JFrame too so that the progress bar doesn't flicker on update.

@neilcsmith-net
Copy link
Copy Markdown
Member

neilcsmith-net commented Mar 30, 2026

On the USE_LAUNCHER_SPLASH = false thing: I could not get this working with HiDPI on Windows in the past. It's also quite a lot of complexity spent on getting the splash screen to show just a tiny bit earlier in the startup cycle.

Well, it also has the benefit of not having WM_CLASS set on it. But I remember the discussion around the issues. I know you can change the image via the API after it's shown, but that's probably not a useful workaround ..

On WM_CLASS: It's good to have this set properly, yes! Is your concern that it is not being set on the splash screen frame, only on the eventual main window?

The problem is not that it isn't set, but that it's set to the default. If you start up the toolkit in the main thread, you'll get the name of the main class, but off another thread like we are, the splash gets WM_CLASS set to java-lang-Thread. See https://github.com/openjdk/jdk/blob/master/src/java.desktop/unix/classes/sun/awt/X11/XToolkit.java#L393

This started being a problem when we stopped using the JDK splash. And why we get duplicate icons in Linux docks if relying solely on matching against WM_CLASS. Of course, we could match Java-lang-Thread but then other Java apps start getting the NetBeans icon. At least GNOME also does some text matching on the name of the desktop file. This is why NBPackage has the ability to customise the desktop file name as well as the desktop StartupWMClass match. It's unknown which Linux desktops this actually works correctly on! See also https://github.com/codelerity/netbeans-packages/blob/main/config/linux-x64-deb.properties#L20

@eirikbakke
Copy link
Copy Markdown
Contributor

eirikbakke commented Mar 30, 2026

This started being a problem when we stopped using the JDK splash. And why we get duplicate icons in Linux docks if relying solely on matching against WM_CLASS.

Oh, interesting. So the JVM splash becomes a possible workaround. There seems to be two options:

  1. Re-enable USE_LAUNCHER_SPLASH on Linux, ideally making it work with HiDPI screens first. The latter would likely require storing a "splash@2x.png" file in the cache directory along with splash.png. (See GetScaledImageName in OpenJDK for the naming convention per JDK-8151787.)

  2. Set WM_SCALE in Splash.java, with the challenge Neil mentioned of finding out what the right value should be.

Might be good for a separate PR after this one is merged, since I suspect the threading and cleanup stuff will make this one complex enough already.

(EDIT Also, is it so that not having WM_CLASS set on the splash screen also solved the duplicate-taskbar-icon problem on Linux? Then a third option would be to un-set WM_SCALE in Splash.java....)

@neilcsmith-net
Copy link
Copy Markdown
Member

Yes, let's get @mbien changes in first, and then consider this problem. Let's keep USE_LAUNCHER_SPLASH as is - different behaviour for different OS is always a pain. We can't unset WM_CLASS, but I would be tempted to move it out of MainWindow and set it as part of the early startup, using a value from the core.startup bundle instead. Can possibly add at https://github.com/apache/netbeans/blob/master/platform/core.startup/src/org/netbeans/core/startup/Main.java#L247 or in Splash::setRunning before the --nosplash check. Former would ensure it's set before the import dialog if need be. Either way, needs to happen after the toolkit is initialized.

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 30, 2026

yep lets try to get the discussion back to this PR. This is primarily about EDT offloading (which is also technically a concurrency bug fix). Secondary is the modality change which can be reverted if there are concerns. Everything else would be followups - which would be appreciated since the code would benefit from more refactoring/cleanups (or a green field rewrite of the splash functionality).

try {
SplashScreen splashScreen = SplashScreen.getSplashScreen();
if (splashScreen != null) {
painter = new SplashPainter(splashScreen.createGraphics(), null, false);
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.

If we are leaving SplashScreen support in, did you test this branch to see that it worked?

Copy link
Copy Markdown
Member Author

@mbien mbien Mar 31, 2026

Choose a reason for hiding this comment

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

a little bit. I am not sure what the original logic was but what happens is the following:

I added a green version of the splash and passed -J-splash:/home/mbien/splash_green.gif to the dev build of this PR. The green version popped up first, a fraction of a second later the regular NB splash appeared over the first. (the green splash had the wrong size, using the png 1:1, so I did see that it remained behind the regular NB splash which is scaled by the Splash class)

note: when I used --spash, the splash (both of them btw) got centered between my two screens. Without it, it centered on the active screen.

keep in mind this happens really fast. If that is the desired effect, it still works.

Copy link
Copy Markdown
Member Author

@mbien mbien Mar 31, 2026

Choose a reason for hiding this comment

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

forgot to mention. i tested this only on linux x11. The regular splash I tested on win11 too. (note: i saw the windows platform launcher had splash related code in it - I chose to ignore that and look away)

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.

when I used --spash, the splash (both of them btw) got centered between my two screens. Without it, it centered on the active screen.

That might be another good reason to just drop "--splash" support.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The green version popped up first, a fraction of a second later the regular NB splash appeared over the first ... so I did see that it remained behind the regular NB splash

That isn't, or at least shouldn't, be what is happening. If the JDK splash window is there, we should be drawing in to it not opening a separate window.

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.

That isn't, or at least shouldn't, be what is happening. If the JDK splash window is there, we should be drawing in to it not opening a separate window.

I was wondering about that but I wasn't sure. I believe both approaches would work (replace vs reuse). In any case this code didn't really change anything there as far as i can tell. NB 29 behaves identical when i pass -J-splash:/home/mbien/splash_green.gif to it

@eirikbakke
Copy link
Copy Markdown
Contributor

eirikbakke commented Mar 31, 2026

I looked through the latest version. Splash.java still has various threading issues. For example:

  • SplashPainter.increment and SplashPainter.addToMaxSteps touches a bunch of fields from the main thread that are also accessed from the EDT in paint().
  • Splash.print, Splash.increment, and Splash.addToMaxSteps all access the non-volatile field "painter" from the main thread, but painter is assigned on the EDT.
  • SplashPainter.setText accesses the non-volatile field "text" from the main thread, but that field is accessed from the EDT in adjustText.
  • Splash.getComponent() is called from a thread other than the EDT and used as the parent of a dialog that is created off-thread.

Threading issues have also clearly been an issue in the past, see e.g. the comment "run in AWT, there were problems with accessing font metrics". So if we touch threading at all we might need to do it properly...

Some things that might help:

  • Making sure that all immutable fields have a "final" keyword, in particular in SplashPainter. (final fields will need to be initialized from a real constructor).
  • Adding a comment before each method and mutable field, indicating which thread is allowed to access them (main or EDT or both). Fields that can be accessed from multiple threads must be accessed through a synchronized block or other tread-safe mechanism (e.g. volatile reference to an immutable value).

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 31, 2026

Threading issues have also clearly been an issue in the past, see e.g. the comment "run in AWT, there were problems with accessing font metrics". So if we touch threading at all we might need to do it properly...

this is a different category of threading problems. One tried to initialize awt and swing code on the main thread which is highly problematic. The other might miss out on an update of an integer increment, which is harmless. Worst case is that the progress is wrong by a few pixels.

I can give this another pass but won't rewrite the whole thing. Esp not while the requirements are unclear given the launchers and the --splash option.

@eirikbakke
Copy link
Copy Markdown
Contributor

I suppose, the alternative to fixing everything "properly" is to just test it for a while. I'd be fine with that, too.

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 31, 2026

this is now no longer a small change since I extracted the model from the UI code.

@eirikbakke
Copy link
Copy Markdown
Contributor

Looks good and latest version runs fine on MacOS. Did anyone test it on Windows already? Happy to dig up my old windows laptop to test.

@neilcsmith-net
Copy link
Copy Markdown
Member

neilcsmith-net commented Mar 31, 2026

Added a PR that should hopefully address the WM_CLASS issue. Should wait and rebase on this.

Will try and test this on Windows.

I have some concerns about the stated behaviour trying the --splash support. That sounds wrong. If it is, pulling it out completely now might simplify this code.

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Mar 31, 2026

Did anyone test it on Windows already?

I tested this on linux X11 and win 11 as previously mentioned

I have some concerns about the stated behaviour trying the --splash support. That sounds wrong. If it is, pulling it out completely now might simplify this code.

I have --splash code pulled out locally already (from Splash class) but its just a few lines, most of it is the comment. I don't plan to actually do that here since the windows platform launcher has splash code in it which I won't touch. There might be more in other places, I haven't really searched that deeply.

will do another push with minor changes in a few minutes since I think I can reduce the diff a little bit and squash.

@neilcsmith-net
Copy link
Copy Markdown
Member

neilcsmith-net commented Apr 1, 2026

I have --splash code pulled out locally already (from Splash class) but its just a few lines, most of it is the comment. I don't plan to actually do that here since the windows platform launcher has splash code in it which I won't touch.

OK, I checked the JDK -splash behaviour and it isn't opening another window but rendering into the existing splash as before, so this is still kind of functional if never used.

Removing the code in here would be more than the comment though - there's lots of comp == null or different graphics checks that are related to supporting both rendering targets. The launcher code isn't hit if the splash file isn't in the cache, and even if there is an image there it would be immediately closed by the window in here opening.

Still, happy with how it is now, too. Thanks!

Copy link
Copy Markdown
Contributor

@eirikbakke eirikbakke left a comment

Choose a reason for hiding this comment

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

Thanks for the edits and cleanup!

@mbien
Copy link
Copy Markdown
Member Author

mbien commented Apr 1, 2026

thanks, will rebase and merge. then continue with the other two startup perf PRs. Cache loading first (#9307, #9308).

the gh action queue is full atm. Will wait until CI runs again.

Removing the code in here would be more than the comment though -

as previously mentioned: already done, here the stash for anyone wanting to do anything with it:

Details
diff --git a/platform/core.startup/src/org/netbeans/core/startup/Splash.java b/platform/core.startup/src/org/netbeans/core/startup/Splash.java
index 416381f..3c80726 100644
--- a/platform/core.startup/src/org/netbeans/core/startup/Splash.java
+++ b/platform/core.startup/src/org/netbeans/core/startup/Splash.java
@@ -32,15 +32,11 @@
 import java.awt.Image;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
-import java.awt.SplashScreen;
 import java.awt.Toolkit;
 import java.awt.Window;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
-import java.awt.image.BufferedImage;
-import java.io.DataOutputStream;
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -51,7 +47,6 @@
 import java.util.StringTokenizer;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import javax.imageio.ImageIO;
 import javax.swing.Icon;
 import javax.swing.JComponent;
 import javax.swing.JDialog;
@@ -62,7 +57,6 @@
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.WindowConstants;
-import org.netbeans.Stamps;
 import org.netbeans.Util;
 import org.openide.util.ImageUtilities;
 import org.openide.util.NbBundle;
@@ -74,18 +68,21 @@
 /** A class that encapsulates all the splash screen things.
 */
 @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
-public final class Splash implements Stamps.Updater {
+public final class Splash {
 
     private static volatile Splash splash;
 
-    private volatile SplashPainter painter;
     private JFrame frame;
-    private SplashComponent comp;
+    private volatile SplashComponent comp;
     private final Progress progress;
 
     /** is there progress bar in splash or not */
     private static final boolean noBar = Boolean.getBoolean("netbeans.splash.nobar") ||
             !Boolean.parseBoolean(NbBundle.getMessage(Splash.class, "SplashShowProgressBar"));
+    
+    private Splash() {
+        this.progress = new Progress();
+    }
 
     public static Splash getInstance() {
         if (splash != null) {
@@ -115,36 +112,7 @@
                 ImageUtilities.loadImage("org/netbeans/core/startup/frame1024.png", true)) // NOI18N
         );
     }
-    /**
-     * Indicate if we should try to take advantage of java's "-splash" parameter, which allows
-     * the splash screen to be displayed at an earlier stage in the app startup sequence. See the
-     * original Buzilla RFE at https://netbeans.org/bugzilla/show_bug.cgi?id=60142 . This requires
-     * splash screen image(s) to be written to the cache directory the first time NetBeans starts,
-     * to be available during subsequent NetBeans startups. Despite
-     * https://bugs.openjdk.java.net/browse/JDK-8145173 and
-     * https://bugs.openjdk.java.net/browse/JDK-8151787 , as of OpenJDK 10.0.2 and OpenJDK 12.0.1
-     * I have found no way to make this work properly with HiDPI screens on Windows. HiDPI filenames
-     * attempted include "splash@2x.png", "splash@200pct.png", "splash.scale-200.png", and
-     * "splash.java-scale200.png". In all of these cases, the regular "splash.png" file is used
-     * instead of one of the 2x-scaled ones (for a system DPI scaling of 200%), and the splash
-     * screen becomes half the expected size. Thus, to we disable this feature for now, in favor of
-     * a slightly delayed splash screen that appears with the correct size and resolution on HiDPI
-     * screens.
-     *
-     * <p>See also https://issues.apache.org/jira/browse/NETBEANS-67 .
-     */
-    private static final boolean USE_LAUNCHER_SPLASH = false;
-    
-    private Splash() {
-        this.progress = new Progress();
-        Stamps s = Stamps.getModulesJARs();
-        if (!CLIOptions.isNoSplash() && !GraphicsEnvironment.isHeadless()) {
-            if (USE_LAUNCHER_SPLASH && !s.exists("splash.png")) {
-                s.scheduleSave(this, "splash.png", false);
-            }
-        }
-    }
-    
+
     int getMaxSteps() {
         return progress.maxSteps;
     }
@@ -162,22 +130,8 @@
         }
         if (show) {
             onEDT(() -> {
-                if (painter == null) {
-                    try {
-                        SplashScreen splashScreen = SplashScreen.getSplashScreen();
-                        if (splashScreen != null) {
-                            painter = new SplashPainter(progress, splashScreen.createGraphics(), null, false);
-                        }
-                    } catch (IllegalStateException splashAlreadyClosed) {}
-                    if (painter == null) {
-                        comp = new SplashComponent(progress, false);
-                        painter = comp.painter;
-                    }
-                    if (comp == null) {
-                        return;
-                    }
-                }
                 if (frame == null) {
+                    comp = new SplashComponent(progress, false);
                     frame = new JFrame(NbBundle.getMessage(Splash.class, "LBL_splash_window_title")); // e.g. for window tray display
                     initFrameIcons(frame); // again, only for possible window tray display
                     frame.setUndecorated(true);
@@ -201,6 +155,7 @@
                     frame.setVisible(false);
                     frame.dispose();
                     frame = null;
+                    comp = null;
                 }
             });
         }
@@ -215,7 +170,7 @@
         if (noBar || CLIOptions.isNoSplash()) {
             return;
         }
-        progress.increment(painter, steps);
+        progress.increment(comp != null ? comp.painter : null, steps);
     }
     
     public Component getComponent() {
@@ -228,7 +183,7 @@
         if (CLIOptions.isNoSplash()) {
             return;
         }
-        progress.setText(painter, text);
+        progress.setText(comp != null ? comp.painter : null, text);
     }
 
     /** Adds specified numbers of steps to a progress
@@ -274,15 +229,6 @@
                 Integer.parseInt(NbBundle.getMessage(Splash.class, "SPLASH_WIDTH")),
                 Integer.parseInt(NbBundle.getMessage(Splash.class, "SPLASH_HEIGHT")));
     }
-
-    @Override
-    public void flushCaches(DataOutputStream os) throws IOException {
-        ImageIO.write((BufferedImage)loadContent(false), "png", os);
-    }
-
-    @Override
-    public void cacheReady() {
-    }
     
     private static void onEDT(Runnable edtAction) {
         if (SwingUtilities.isEventDispatchThread()) {
@@ -553,12 +499,6 @@
             } else {
                 if (next < System.currentTimeMillis()) {
                     paint();
-                    try {
-                        SplashScreen ss = SplashScreen.getSplashScreen();
-                        if (ss != null) {
-                            ss.update();
-                        }
-                    } catch (IllegalStateException splashAlreadyClosed) {}
                     next = System.currentTimeMillis() + 200;
                 }
             }

 - about 100ms faster startup (main method duration measured) and
   technically more correct anyway
 - use JFrame to get double buffering (no progress bar flicker during
   repaint) and we can use the opportunity to initialize swing
   asynchronously while NB is busy bootstrapping.
 - other change: splash window is no longer modal
 - code renovations

most changes come from the fact that the model needed to be extracted
from UI code so that it can be updated from any thread while the UI
paints on EDT.
@mbien mbien merged commit f186217 into apache:master Apr 1, 2026
31 checks passed
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) Code cleanup Label for cleanup done on the Netbeans IDE performance Platform [ci] enable platform tests (platform/*) UI User Interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Splash screen always on top of everything

3 participants