This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 4db25ecd4d issue #7762: cache SVG images in SwtGc to stop SWT handle 
leaks (#7763)
4db25ecd4d is described below

commit 4db25ecd4d5a07830358db48ea88f0c00043f582
Author: Matt Casters <[email protected]>
AuthorDate: Mon Aug 3 14:46:21 2026 +0200

    issue #7762: cache SVG images in SwtGc to stop SWT handle leaks (#7763)
    
    drawImage(SvgFile) created a new SwtUniversalImageSvg (and SWT Image
    bitmaps) on every paint and never disposed them. Large canvases that
    draw many SVG icons per frame (e.g. while dragging nodes) exhaust native
    handles and throw SWTError: No More handles.
    
    Cache universal images by filename and dark-mode flag, dispose them when
    the Display shuts down, and expose getNativeGc() for shared bitmap paint.
---
 .../org/apache/hop/ui/hopgui/shared/SwtGc.java     | 69 ++++++++++++++++++++--
 1 file changed, 65 insertions(+), 4 deletions(-)

diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
index 14150279f9..9edaca5fe6 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
@@ -19,6 +19,8 @@ package org.apache.hop.ui.hopgui.shared;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import org.apache.hop.core.SwtUniversalImage;
 import org.apache.hop.core.SwtUniversalImageSvg;
 import org.apache.hop.core.exception.HopException;
@@ -35,6 +37,7 @@ import org.apache.hop.ui.util.EnvironmentUtils;
 import org.apache.hop.workflow.action.ActionMeta;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.graphics.Image;
@@ -42,9 +45,23 @@ import org.eclipse.swt.graphics.LineAttributes;
 import org.eclipse.swt.graphics.RGB;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Display;
 
 public class SwtGc implements IGc {
 
+  /**
+   * Cached {@link SwtUniversalImageSvg} instances used by {@link 
#drawImage(SvgFile, int, int, int,
+   * int, float, double)}. Without this cache, every canvas paint creates a 
new universal image (and
+   * its SWT {@link Image} bitmaps) that is never disposed — a severe handle 
leak on large graphs
+   * (e.g. Data Vault models with many table icons redrawn while dragging).
+   *
+   * <p>Keyed by SVG filename and dark-mode flag so theme changes get a 
correct rendering.
+   */
+  private static final Map<String, SwtUniversalImage> SVG_IMAGE_CACHE = new 
ConcurrentHashMap<>();
+
+  private static final Object SVG_CACHE_HOOK_LOCK = new Object();
+  private static volatile boolean svgCacheDisposeHookRegistered;
+
   protected Color background;
 
   protected Color black;
@@ -110,6 +127,15 @@ public class SwtGc implements IGc {
     this.deprecated = GuiResource.getInstance().getColorDeprecated();
   }
 
+  /**
+   * Underlying SWT graphics context. Callers must not dispose it; ownership 
stays with the canvas
+   * paint event (or the double-buffer image GC). Useful for painting 
shared/cached {@link Image}
+   * bitmaps without going through {@link #drawImage(SvgFile, int, int, int, 
int, float, double)}.
+   */
+  public GC getNativeGc() {
+    return gc;
+  }
+
   @Override
   public void dispose() {
     // Do not dispose the GC.  It's handed to us so we don't dispose it
@@ -508,10 +534,7 @@ public class SwtGc implements IGc {
       float magnification,
       double angle)
       throws HopException {
-    //
-    SvgCacheEntry cacheEntry = SvgCache.loadSvg(svgFile);
-    SwtUniversalImageSvg imageSvg =
-        new SwtUniversalImageSvg(new SvgImage(cacheEntry.getSvgDocument()));
+    SwtUniversalImage imageSvg = getCachedSvgImage(svgFile);
 
     int magnifiedWidth = Math.round(desiredWidth * magnification);
     int magnifiedHeight = Math.round(desiredHeight * magnification);
@@ -534,6 +557,44 @@ public class SwtGc implements IGc {
     }
   }
 
+  /**
+   * Returns a process-wide cached {@link SwtUniversalImage} for the given SVG 
file. Callers must
+   * not dispose the returned instance.
+   */
+  private SwtUniversalImage getCachedSvgImage(SvgFile svgFile) throws 
HopException {
+    SvgCacheEntry cacheEntry = SvgCache.loadSvg(svgFile);
+    boolean darkMode = PropsUi.getInstance().isDarkMode();
+    String cacheKey = svgFile.getFilename() + (darkMode ? "|dark" : "|light");
+    ensureSvgImageCacheDisposeHook(gc.getDevice());
+    return SVG_IMAGE_CACHE.computeIfAbsent(
+        cacheKey,
+        key -> new SwtUniversalImageSvg(new 
SvgImage(cacheEntry.getSvgDocument()), false));
+  }
+
+  private static void ensureSvgImageCacheDisposeHook(Device device) {
+    if (svgCacheDisposeHookRegistered || !(device instanceof Display display)) 
{
+      return;
+    }
+    synchronized (SVG_CACHE_HOOK_LOCK) {
+      if (svgCacheDisposeHookRegistered) {
+        return;
+      }
+      display.addListener(
+          SWT.Dispose,
+          event -> {
+            for (SwtUniversalImage image : SVG_IMAGE_CACHE.values()) {
+              try {
+                image.dispose();
+              } catch (Exception ignored) {
+                // best-effort cleanup at display shutdown
+              }
+            }
+            SVG_IMAGE_CACHE.clear();
+          });
+      svgCacheDisposeHookRegistered = true;
+    }
+  }
+
   @Override
   public boolean drawFileImage(String path, int x, int y, int width, int 
height) {
     if (path == null || path.isEmpty() || width <= 0 || height <= 0) {

Reply via email to