Skip to content

Commit 29df77f

Browse files
committed
Fix leaking fonts in ConsoleZoomHandler
- Don't use TextConsole attributes for storing fonts, the attributes will be cleaned up on console dispose - Don't run too many tasks async, only those originating from non-UI thread - collect created fonts to dispose after console closes - dispose created console fonts only after console is removed and with a delay to prevent possibly still running StyledText layout operation to run into errors. Fixes #2844
1 parent 40b0730 commit 29df77f

2 files changed

Lines changed: 37 additions & 67 deletions

File tree

debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,13 +463,11 @@ public void testRemovingConsoleDisposesZoomFont(TestInfo testInfo) throws Except
463463
ConsoleZoomHandler.applyZoom(consoleView, 1);
464464
TestUtil.processUIEvents();
465465

466-
Object attribute = console.getAttribute(ConsoleZoomHandler.ZOOM_FONT_ATTRIBUTE);
467-
assertThat(attribute).as("a custom zoom font should have been created").isInstanceOf(Font.class); //$NON-NLS-1$
468-
Font zoomFont = (Font) attribute;
466+
Font zoomFont = console.getFont();
469467
assertFalse(zoomFont.isDisposed(), "the zoom font must not be disposed while its console is still open"); //$NON-NLS-1$
470468

471469
removeConsoles(console);
472-
TestUtil.processUIEvents();
470+
TestUtil.processUIEvents(200);
473471

474472
assertTrue(zoomFont.isDisposed(), "the zoom font must be disposed once its console is removed"); //$NON-NLS-1$
475473
} finally {

debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleZoomHandler.java

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
*******************************************************************************/
1414
package org.eclipse.ui.internal.console;
1515

16+
import java.util.ArrayList;
1617
import java.util.HashMap;
18+
import java.util.List;
1719
import java.util.Map;
1820
import java.util.concurrent.ConcurrentHashMap;
1921

@@ -62,10 +64,9 @@
6264
public class ConsoleZoomHandler extends AbstractHandler implements IExecutableExtension {
6365

6466
/**
65-
* Key used to remember, on the console itself, the custom font created for
66-
* zooming, so it can be reused/replaced and eventually disposed.
67+
* Remember the custom fonts created for each console, so they can be disposed.
6768
*/
68-
public static final String ZOOM_FONT_ATTRIBUTE = ConsoleZoomHandler.class.getName() + ".zoomFont"; //$NON-NLS-1$
69+
private static final Map<TextConsole, List<Font>> fontsMap = new HashMap<>();
6970

7071
/**
7172
* Key used to remember, on the console itself, that a mismatching font change
@@ -151,7 +152,7 @@ public void consolesAdded(IConsole[] consoles) {
151152
textConsole.addPropertyChangeListener(FONT_ENFORCER);
152153
ZoomState state = sZoomByType.get(typeKey(textConsole));
153154
if (state != null) {
154-
applyHeight(textConsole, state.height());
155+
Display.getDefault().asyncExec(() -> applyHeight(textConsole, state.height()));
155156
}
156157
}
157158
}
@@ -162,7 +163,7 @@ public void consolesRemoved(IConsole[] consoles) {
162163
for (IConsole console : consoles) {
163164
if (console instanceof TextConsole textConsole) {
164165
textConsole.removePropertyChangeListener(FONT_ENFORCER);
165-
disposeZoomFont(textConsole);
166+
Display.getDefault().asyncExec(() -> disposeZoomFonts(textConsole));
166167
}
167168
}
168169
}
@@ -221,10 +222,6 @@ private static void onFontChanged(TextConsole textConsole) {
221222
return;
222223
}
223224
}
224-
// genuine external change: our own custom zoom font, if any, is no longer
225-
// the console's active font, so it must be disposed now instead of being
226-
// leaked until the console (e.g. a long-lived ProcessConsole) is removed
227-
disposeZoomFont(textConsole);
228225
sZoomByType.put(type, new ZoomState(currentHeight.intValue(), 0));
229226
persistZoomStates();
230227
}
@@ -317,11 +314,6 @@ public Object execute(ExecutionEvent event) throws ExecutionException {
317314
* @param delta the font size delta to apply, in points
318315
*/
319316
public static void applyZoom(IWorkbenchPart part, int delta) {
320-
if (Display.getCurrent() == null) {
321-
Display.getDefault().asyncExec(() -> applyZoom(part, delta));
322-
return;
323-
}
324-
325317
if (!(part instanceof IConsoleView consoleView)) {
326318
return;
327319
}
@@ -390,11 +382,6 @@ private static Integer getFontHeight(TextConsole textConsole) {
390382
* @param height the font height to apply, in points
391383
*/
392384
private static void applyHeight(TextConsole textConsole, int height) {
393-
if (Display.getCurrent() == null) {
394-
Display.getDefault().asyncExec(() -> applyHeight(textConsole, height));
395-
return;
396-
}
397-
398385
// make sure this console's font is (still) being watched, in case it was
399386
// registered before the zoom handler class got loaded, or the listener
400387
// was otherwise not yet attached
@@ -415,61 +402,46 @@ private static void applyHeight(TextConsole textConsole, int height) {
415402
fd.setHeight(height);
416403
}
417404

418-
Object oldAttribute = textConsole.getAttribute(ZOOM_FONT_ATTRIBUTE);
419-
Font oldZoomFont = oldAttribute instanceof Font f ? f : null;
420-
421-
Font newZoomFont = new Font(currentFont.getDevice(), fontData);
422-
// remember/apply before disposing the old one, in case they are the same object
423-
textConsole.setAttribute(ZOOM_FONT_ATTRIBUTE, newZoomFont);
424-
textConsole.setFont(newZoomFont);
425-
426-
if (oldZoomFont != null && !oldZoomFont.isDisposed()) {
427-
disposeLater(oldZoomFont);
428-
}
429-
}
430-
431-
/**
432-
* Disposes the custom zoom font remembered on the given console, if any.
433-
* Dispatches to the UI thread if necessary.
434-
*
435-
* @param textConsole the console whose zoom font should be disposed
436-
*/
437-
private static void disposeZoomFont(TextConsole textConsole) {
438-
Object attribute = textConsole.getAttribute(ZOOM_FONT_ATTRIBUTE);
439-
if (!(attribute instanceof Font font) || font.isDisposed()) {
440-
return;
441-
}
442-
if (Display.getCurrent() == null) {
443-
// see applyHeight(...) above for why asyncExec (not syncExec) is used
444-
Display.getDefault().asyncExec(() -> disposeZoomFont(textConsole));
445-
return;
446-
}
447-
disposeLater(font);
448-
textConsole.setAttribute(ZOOM_FONT_ATTRIBUTE, null);
405+
fontsMap.compute(textConsole, (console, oldZoomFonts) -> {
406+
Font newZoomFont = new Font(currentFont.getDevice(), fontData);
407+
textConsole.setFont(newZoomFont);
408+
List<Font> oldFonts = oldZoomFonts;
409+
if (oldFonts == null) {
410+
oldFonts = new ArrayList<>();
411+
}
412+
oldFonts.add(newZoomFont);
413+
return oldFonts;
414+
});
449415
}
450416

451417
/**
452-
* Disposes the given font on a later UI cycle rather than immediately.
418+
* Disposes all custom fonts after console is removed on a later UI cycle rather
419+
* than immediately.
453420
* <p>
454421
* A font that was just replaced on a console (e.g. via
455-
* {@link TextConsole#setFont(Font)}) may still be referenced for a little
456-
* while by the viewer's internal rendering caches (e.g.
422+
* {@link TextConsole#setFont(Font)}) may still be referenced for a little while
423+
* by the viewer's internal rendering caches (e.g.
457424
* {@code StyledText}/{@code TextLayout} keep per-line layouts that are only
458425
* refreshed on their next repaint). Disposing it synchronously can therefore
459426
* cause a later, asynchronously dispatched repaint to fail with an
460-
* {@code IllegalArgumentException} ("Argument not valid") when it tries to
461-
* use the now-disposed font. Deferring the actual disposal by one UI cycle
462-
* gives any such pending repaint a chance to pick up the new font first.
427+
* {@code IllegalArgumentException} ("Argument not valid") when it tries to use
428+
* the now-disposed font. Deferring the actual disposal by one UI cycle gives
429+
* any such pending repaint a chance to pick up the new font first.
463430
* </p>
464431
*
465-
* @param font the font to dispose; must be called on the UI thread
432+
* @param textConsole the console whose zoom fonts should be disposed
466433
*/
467-
private static void disposeLater(Font font) {
468-
Display.getCurrent().asyncExec(() -> {
469-
if (!font.isDisposed()) {
470-
font.dispose();
471-
}
472-
});
434+
private static void disposeZoomFonts(TextConsole textConsole) {
435+
List<Font> oldFonts = fontsMap.remove(textConsole);
436+
if (oldFonts != null && !oldFonts.isEmpty()) {
437+
Display.getDefault().timerExec(100, () -> {
438+
for (Font font : oldFonts) {
439+
if (font != null && !font.isDisposed()) {
440+
font.dispose();
441+
}
442+
}
443+
});
444+
}
473445
}
474446
}
475447

0 commit comments

Comments
 (0)