From 4c876d2e725523b0ba698e55ccd0d72e22177034 Mon Sep 17 00:00:00 2001 From: Becky Auger-Williams Date: Fri, 24 Apr 2026 11:32:43 +0100 Subject: [PATCH 1/5] Turn UI update thread into a singleton so all updates across all windows come from a single thread --- .../RepresentationUpdateThrottle.java | 18 +++++++++++++++++- .../representation/ToolkitRepresentation.java | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java index a286a27c3a..c5a8be8981 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java @@ -53,6 +53,9 @@ public class RepresentationUpdateThrottle /** Pause between updates to prevent flooding the UI thread */ private static final long update_delay = Preferences.update_delay; + /** Singleton instance of this class */ + private static RepresentationUpdateThrottle INSTANCE; + /** Executor for UI thread */ private final Executor gui_executor; @@ -73,8 +76,21 @@ public class RepresentationUpdateThrottle */ private final Set> updateable = new LinkedHashSet<>(); + /** Get instance of this class to perform updates on the UI thread. This class + * is a singleton to ensure that only one thread is scheduling jobs on the UI + * thread. + * + * @param gui_executor Executor for UI thread + */ + public static RepresentationUpdateThrottle getInstance(final Executor gui_executor) { + if(INSTANCE == null) { + INSTANCE = new RepresentationUpdateThrottle(gui_executor); + } + return INSTANCE; + } + /** @param gui_executor Executor for UI thread */ - public RepresentationUpdateThrottle(final Executor gui_executor) + private RepresentationUpdateThrottle(final Executor gui_executor) { final String name = "RepresentationUpdateThrottle" + instance.incrementAndGet(); logger.log(Level.FINE, "Create " + name); diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/ToolkitRepresentation.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/ToolkitRepresentation.java index 957fcbd6ee..ee26bc515b 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/ToolkitRepresentation.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/ToolkitRepresentation.java @@ -74,7 +74,7 @@ abstract public class ToolkitRepresentation implements E private final boolean edit_mode; - private final RepresentationUpdateThrottle throttle = new RepresentationUpdateThrottle(this); + private final RepresentationUpdateThrottle throttle = RepresentationUpdateThrottle.getInstance(this); /** * Listener list From db7a5b0d2202f85707919745377fc532702fbfe8 Mon Sep 17 00:00:00 2001 From: Becky Auger-Williams Date: Fri, 24 Apr 2026 14:17:16 +0100 Subject: [PATCH 2/5] Fix test to use new UpdateThrottle singleton --- .../display/builder/representation/UpdateThrottleTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/display/representation/src/test/java/org/csstudio/display/builder/representation/UpdateThrottleTest.java b/app/display/representation/src/test/java/org/csstudio/display/builder/representation/UpdateThrottleTest.java index d7112f7797..085513d876 100644 --- a/app/display/representation/src/test/java/org/csstudio/display/builder/representation/UpdateThrottleTest.java +++ b/app/display/representation/src/test/java/org/csstudio/display/builder/representation/UpdateThrottleTest.java @@ -28,7 +28,7 @@ @SuppressWarnings("nls") public class UpdateThrottleTest { - private final RepresentationUpdateThrottle throttle = new RepresentationUpdateThrottle(Executors.newSingleThreadExecutor()); + private final RepresentationUpdateThrottle throttle = RepresentationUpdateThrottle.getInstance(Executors.newSingleThreadExecutor()); private class TestWidgetRepresentation extends WidgetRepresentation { From 50d937e6af7fa08713d0819b81d74775940f1027 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Mon, 27 Apr 2026 10:12:53 +0100 Subject: [PATCH 3/5] Rename INSTANCE to instance and remove previous definition as we now only have a single UpdateThrottle and so do not need to name the instance based on how many there are. --- .../RepresentationUpdateThrottle.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java index c5a8be8981..d1b7ba28c1 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java @@ -38,9 +38,6 @@ @SuppressWarnings("nls") public class RepresentationUpdateThrottle { - /** Instance counter to aid in debugging the throttle start/shutdown */ - private static final AtomicInteger instance = new AtomicInteger(); - /** Period in seconds for logging update performance */ private static final int performance_log_period_secs = Preferences.performance_log_period_secs; @@ -54,7 +51,7 @@ public class RepresentationUpdateThrottle private static final long update_delay = Preferences.update_delay; /** Singleton instance of this class */ - private static RepresentationUpdateThrottle INSTANCE; + private static RepresentationUpdateThrottle instance; /** Executor for UI thread */ private final Executor gui_executor; @@ -83,16 +80,16 @@ public class RepresentationUpdateThrottle * @param gui_executor Executor for UI thread */ public static RepresentationUpdateThrottle getInstance(final Executor gui_executor) { - if(INSTANCE == null) { - INSTANCE = new RepresentationUpdateThrottle(gui_executor); + if(instance == null) { + instance = new RepresentationUpdateThrottle(gui_executor); } - return INSTANCE; + return instance; } /** @param gui_executor Executor for UI thread */ private RepresentationUpdateThrottle(final Executor gui_executor) { - final String name = "RepresentationUpdateThrottle" + instance.incrementAndGet(); + final String name = "RepresentationUpdateThrottle"; logger.log(Level.FINE, "Create " + name); this.gui_executor = gui_executor; throttle_thread = new Thread(this::doRun); From df482b8c2aab857d2c3b114516d9d8da55ba99c7 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Wed, 29 Apr 2026 18:04:11 +0100 Subject: [PATCH 4/5] Only shutdown UpdateThrottle when last reference has been closed --- .../representation/RepresentationUpdateThrottle.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java index d1b7ba28c1..e827336126 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java @@ -38,6 +38,9 @@ @SuppressWarnings("nls") public class RepresentationUpdateThrottle { + /** Reference counter to determine when to safely shutdown */ + private static final AtomicInteger reference_count = new AtomicInteger(); + /** Period in seconds for logging update performance */ private static final int performance_log_period_secs = Preferences.performance_log_period_secs; @@ -83,6 +86,7 @@ public static RepresentationUpdateThrottle getInstance(final Executor gui_execut if(instance == null) { instance = new RepresentationUpdateThrottle(gui_executor); } + reference_count.incrementAndGet(); return instance; } @@ -231,7 +235,13 @@ private void updateInUI(final WidgetRepresentation[] representations, /** Shutdown the throttle thread and wait for it to exit */ public void shutdown() { + // Only shutdown if this is the last reference + if (reference_count.decrementAndGet() != 0){ + return; + } + run = false; + instance = null; synchronized (updateable) { updateable.notifyAll(); From 7220cfa749d02fc961c928221f1f55cf3638ccb1 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Thu, 30 Apr 2026 15:43:49 +0100 Subject: [PATCH 5/5] Remove shutdown of the singleton UpdateThrottle and maintain reference count for debugging purposes. The class will be created when the first display is opened and will not be shutdown until Phoebus is closed. --- .../RepresentationUpdateThrottle.java | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java index e827336126..71a0c8ce9a 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/RepresentationUpdateThrottle.java @@ -38,7 +38,7 @@ @SuppressWarnings("nls") public class RepresentationUpdateThrottle { - /** Reference counter to determine when to safely shutdown */ + /** Reference counter to aid in debugging the throttle start/shutdown */ private static final AtomicInteger reference_count = new AtomicInteger(); /** Period in seconds for logging update performance */ @@ -80,22 +80,22 @@ public class RepresentationUpdateThrottle * is a singleton to ensure that only one thread is scheduling jobs on the UI * thread. * - * @param gui_executor Executor for UI thread + * @param guiExecutor Executor for UI thread */ - public static RepresentationUpdateThrottle getInstance(final Executor gui_executor) { + public static synchronized RepresentationUpdateThrottle getInstance(final Executor guiExecutor) { if(instance == null) { - instance = new RepresentationUpdateThrottle(gui_executor); + instance = new RepresentationUpdateThrottle(guiExecutor); } reference_count.incrementAndGet(); return instance; } - /** @param gui_executor Executor for UI thread */ - private RepresentationUpdateThrottle(final Executor gui_executor) + /** @param guiExecutor Executor for UI thread */ + private RepresentationUpdateThrottle(final Executor guiExecutor) { final String name = "RepresentationUpdateThrottle"; logger.log(Level.FINE, "Create " + name); - this.gui_executor = gui_executor; + this.gui_executor = guiExecutor; throttle_thread = new Thread(this::doRun); throttle_thread.setName(name); throttle_thread.setDaemon(true); @@ -235,26 +235,6 @@ private void updateInUI(final WidgetRepresentation[] representations, /** Shutdown the throttle thread and wait for it to exit */ public void shutdown() { - // Only shutdown if this is the last reference - if (reference_count.decrementAndGet() != 0){ - return; - } - - run = false; - instance = null; - synchronized (updateable) - { - updateable.notifyAll(); - } - try - { - throttle_thread.join(2000); - } - catch (final InterruptedException ex) - { - // Ignore, closing down anyway - } - if (throttle_thread.isAlive()) - logger.log(Level.WARNING, "Representation update throttle fails to terminate within 2 seconds"); + reference_count.decrementAndGet(); } }