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) {