Summary
On GTK3, ToolBar.computeSize unconditionally toggles gtk_toolbar_set_show_arrow(handle, false) before native measurement and, for SWT.WRAP toolbars, toggles it back to true afterwards. Each state change inside gtk_toolbar_set_show_arrow calls gtk_widget_queue_resize → gtk_widget_queue_draw → gdk_window_invalidate_region on the toolbar's GdkWindow.
When a SWT.WRAP ToolBar is placed inside a parent that gets measured on every layout pass — for example, the Composite passed to CTabFolder.setTopRight(...) — the invalidation reschedules the next paint at vsync, which triggers another layout pass, which calls ToolBar.computeSize again, which fires another invalidation. A self-sustaining ~60 Hz repaint loop forms that consumes ~10–25 % of one CPU core while the application is completely idle. The loop runs whenever the window is mapped and only stops when the window is unmapped (minimised) or the offending parent is disposed.
This is a latent bug that has been dormant in SWT since the GTK3 implementation of SWT.WRAP was added in bug 46025 (SWT 3.8 M7, 2012). It is reproducible in SWT 3.130.0 on stock Ubuntu 25.10 with stock GTK 3.24.18.
Environment
|
|
| SWT version |
org.eclipse.swt.gtk.linux.x86_64 3.130.0 |
| Java |
OpenJDK 23.0.2 (Temurin / JustJ) |
| OS |
Ubuntu 25.10 (kernel 6.17) |
| GTK |
3.24.18 (libgdk-3.so.0.2418.32) |
| Desktop |
GNOME (also reproducible on Wayland) |
Minimal reproducer
See ToolBarWrapRepaintLoop.java attached. Pure SWT, no dependencies, ~55 lines. Toggle SWT.WRAP in the TOOLBAR_STYLE constant to reproduce vs fix.
final int TOOLBAR_STYLE = SWT.FLAT | SWT.RIGHT | SWT.WRAP;
// ... creates a Shell with a CTabFolder whose topRight holds a ToolBar(TOOLBAR_STYLE).
With SWT.WRAP set — top -H -p <pid> shows the SWT main thread at ~10–25 % CPU continuously, with zero user input and no visible change on screen. Remains high as long as the window is mapped. Drops to 0 % if the window is minimised.
Without SWT.WRAP — main sits at 0 % CPU on idle, as expected.
Root cause
ToolBar.computeSizeInPixels in SWT 3.130.0 (bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.java around lines 180–196):
if (GTK.GTK4) {
size = computeNativeSize (handle, wHint, hHint, changed);
} else {
/*
* Feature in GTK. Size of toolbar is calculated incorrectly
* and appears as just the overflow arrow, if the arrow is enabled
* to display. The fix is to disable it before the computation of
* size and enable it if WRAP style is set.
*/
GTK3.gtk_toolbar_set_show_arrow (handle, false);
size = computeNativeSize (handle, wHint, hHint, changed);
if ((style & SWT.WRAP) != 0) GTK3.gtk_toolbar_set_show_arrow (handle, true);
}
The set_show_arrow(false) call is the workaround for the GTK sizing bug the comment describes. For non-WRAP toolbars, show_arrow is always false, so the first set_show_arrow(false) call is a no-op inside GTK and nothing is invalidated.
For WRAP toolbars, SWT re-enables show_arrow after measurement by calling set_show_arrow(true). On the next measurement, the state cycle becomes true → set(false) → false → measure → set(true) → true. Each of those two state transitions fires this chain inside GTK:
gtk_toolbar_set_show_arrow
→ gtk_widget_queue_resize
→ gtk_widget_queue_draw
→ gdk_window_invalidate_region
which marks the toolbar's GdkWindow dirty and schedules a paint for the next frame.
The feedback loop forms whenever the WRAP ToolBar sits in a parent that CTabFolder or another SWT container measures on every layout pass. CTabFolder.setTopRight(Composite) is the canonical case — CTabFolder.onResize / layout pass → topRight.computeSize → GridLayout.layout → ToolBar.computeSize → toxic toggle → invalidation → next frame → back to CTabFolder.onResize. The cycle repeats at vsync rate.
Evidence — bpftrace uprobe trace
Attached uprobes on gdk_window_invalidate_rect / gdk_window_invalidate_region in a running NetXMS management console (one of many SWT applications that uses CTabFolder with a SWT.WRAP top-right toolbar). 10-second capture:
$ sudo bpftrace -p $PID -e '
uprobe:/usr/lib/x86_64-linux-gnu/libgdk-3.so.0:gdk_window_invalidate_rect { @rect[ustack(16)] = count(); }
uprobe:/usr/lib/x86_64-linux-gnu/libgdk-3.so.0:gdk_window_invalidate_region { @region[ustack(16)] = count(); }
interval:s:10 { exit(); }'
@region[
gdk_window_invalidate_region+0
gtk_widget_queue_draw+150
gtk_widget_queue_resize+120
gtk_toolbar_set_show_arrow+110
Java_org_eclipse_swt_internal_gtk3_GTK3_gtk_1toolbar_1set_1show_1arrow+15
0x7cfd1112c3dc
0x70111e838
]: 586
@region[
gdk_window_invalidate_region+0
gtk_widget_queue_draw+150
gtk_widget_queue_resize+120
gtk_toolbar_set_show_arrow+110
Java_org_eclipse_swt_internal_gtk3_GTK3_gtk_1toolbar_1set_1show_1arrow+15
0x7cfd1112c3dc
0xa00111e838
]: 586
@region[
gdk_window_invalidate_region+0
gtk_widget_queue_draw+150
gtk_widget_queue_resize+120
gtk_toolbar_set_show_arrow+110
Java_org_eclipse_swt_internal_gtk3_GTK3_gtk_1toolbar_1set_1show_1arrow+15
0x7cfd1112c2ec
]: 1172
2344 invalidations in 10 s = ~234 per second, all from Java_org_eclipse_swt_internal_gtk3_GTK3_gtk_1toolbar_1set_1show_1arrow. No other callers appear in the profile.
Evidence — instrumented CTabFolderRenderer
Substituting a CTabFolderRenderer subclass that counts draw(PART_HEADER) calls and logs a Java stack trace every N calls produced this sample log line (NetXMS console, 9-tab perspective):
WARN CTabFolderRenderer - PART_HEADER draws: 119 in 2000ms (59.5/s)
folder=2054757222 itemCount=9 size=Point {1260, 1278} parent=ViewFolder
latest stack:
at org.eclipse.swt.custom.CTabFolder.onPaint(CTabFolder.java:2091)
at org.eclipse.swt.custom.CTabFolder.lambda$0(CTabFolder.java:338)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5862)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Control.gtk_draw(Control.java:3891)
at org.eclipse.swt.widgets.Composite.gtk_draw(Composite.java:506)
at org.eclipse.swt.widgets.Widget.windowProc(Widget.java:2614)
at org.eclipse.swt.widgets.Control.windowProc(Control.java:6857)
at org.eclipse.swt.widgets.Display.windowProc(Display.java:6169)
at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_do_event(Native Method)
at org.eclipse.swt.widgets.Display.eventProc(Display.java:1605)
at org.eclipse.swt.internal.gtk3.GTK3.gtk_main_iteration_do(Native Method)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4519)
59.5 paints per second — vsync. No Java application code appears above draw(); every paint enters through gtk_main_do_event → Display.readAndDispatch. The application is not calling redraw(), layout(), or anything else — the entire loop runs below the Java layer and re-enters SWT only for rendering.
Proposed fixes
In approximate order of invasiveness — any one of these would break the loop:
1. Cache the computed size and avoid redundant toggles. The simplest fix. ToolBar.computeSizeInPixels can cache the most recent (wHint, hHint, state) → size result and return it without calling native measurement if neither the items nor the hints have changed. A changed == true argument would invalidate the cache. This also benefits Win32 / Cocoa code paths where native measurement has a non-trivial cost.
2. Guard the set_show_arrow(true) re-enable with a state check. Track SWT's "desired show_arrow state" in a Java field; only call gtk_toolbar_set_show_arrow(handle, true) if SWT's field says the widget should currently have show_arrow = true and the previous call flipped it off. Avoid unconditionally re-enabling it after every measurement. This is the smallest diff but doesn't address the first set_show_arrow(false) call when the previous state was already false (which is the no-op case in GTK and costs nothing anyway).
3. Replace the toggle workaround with a direct gtk_widget_get_preferred_width/_height measurement. The comment in ToolBar.computeSizeInPixels explains that set_show_arrow(false) works around a GTK sizing bug. GTK3's preferred-size API has been stable since ~3.10 and the original workaround may no longer be necessary. If gtk_widget_get_preferred_size returns the correct dimensions for a WRAP toolbar without the show_arrow dance, the entire toggle path can be removed on GTK3.
4. Document the gotcha. If none of the above are accepted, at minimum CTabFolder.setTopRight and ToolBar should document that placing a SWT.WRAP toolbar in a frequently-measured parent on GTK3 will cause a repaint loop. Every SWT-on-GTK3 application is currently affected without knowing it.
Downstream workaround
Applications that cannot wait for an SWT release can work around the bug by not using SWT.WRAP on any ToolBar placed inside CTabFolder.setTopRight(...) or any similarly-measured parent. The cost is the loss of the overflow chevron; items that don't fit are clipped instead of hidden behind a dropdown. In practice most SWT-based UIs have ≤ 5 items in their top-right toolbar and fit trivially, so the visible regression is nil.
Impact estimate
Any SWT-on-GTK3 application that uses CTabFolder.setTopRight(Composite) with a SWT.WRAP-styled ToolBar child — which is the idiomatic pattern in Eclipse IDE, NetXMS nxmc, and many JFace-based applications — is silently burning 10–25 % of one CPU core per open tab folder. The cost is not visible in Java Flight Recorder / YourKit / JProfiler because the loop originates in GTK and re-enters Java only through the SWT event loop; standard Java profiling reports "the main thread is in Display.sleep() / gtk_main_iteration_do" and does not flag the per-frame measurement cost.
Users experience this as battery drain on laptops, fans running on workstations, and the SWT main thread being less responsive under load. The root cause has been dormant since 2012 and is unlikely to be discovered without tracing at the GDK level.
ToolBarWrapRepaintLoop.java
Summary
On GTK3,
ToolBar.computeSizeunconditionally togglesgtk_toolbar_set_show_arrow(handle, false)before native measurement and, forSWT.WRAPtoolbars, toggles it back totrueafterwards. Each state change insidegtk_toolbar_set_show_arrowcallsgtk_widget_queue_resize → gtk_widget_queue_draw → gdk_window_invalidate_regionon the toolbar'sGdkWindow.When a
SWT.WRAPToolBaris placed inside a parent that gets measured on every layout pass — for example, theCompositepassed toCTabFolder.setTopRight(...)— the invalidation reschedules the next paint at vsync, which triggers another layout pass, which callsToolBar.computeSizeagain, which fires another invalidation. A self-sustaining ~60 Hz repaint loop forms that consumes ~10–25 % of one CPU core while the application is completely idle. The loop runs whenever the window is mapped and only stops when the window is unmapped (minimised) or the offending parent is disposed.This is a latent bug that has been dormant in SWT since the GTK3 implementation of
SWT.WRAPwas added in bug 46025 (SWT 3.8 M7, 2012). It is reproducible in SWT 3.130.0 on stock Ubuntu 25.10 with stock GTK 3.24.18.Environment
org.eclipse.swt.gtk.linux.x86_643.130.0libgdk-3.so.0.2418.32)Minimal reproducer
See
ToolBarWrapRepaintLoop.javaattached. Pure SWT, no dependencies, ~55 lines. ToggleSWT.WRAPin theTOOLBAR_STYLEconstant to reproduce vs fix.With
SWT.WRAPset —top -H -p <pid>shows the SWTmainthread at ~10–25 % CPU continuously, with zero user input and no visible change on screen. Remains high as long as the window is mapped. Drops to 0 % if the window is minimised.Without
SWT.WRAP—mainsits at 0 % CPU on idle, as expected.Root cause
ToolBar.computeSizeInPixelsin SWT 3.130.0 (bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.javaaround lines 180–196):The
set_show_arrow(false)call is the workaround for the GTK sizing bug the comment describes. For non-WRAP toolbars,show_arrowis alwaysfalse, so the firstset_show_arrow(false)call is a no-op inside GTK and nothing is invalidated.For WRAP toolbars, SWT re-enables
show_arrowafter measurement by callingset_show_arrow(true). On the next measurement, the state cycle becomestrue → set(false) → false → measure → set(true) → true. Each of those two state transitions fires this chain inside GTK:which marks the toolbar's
GdkWindowdirty and schedules a paint for the next frame.The feedback loop forms whenever the WRAP
ToolBarsits in a parent thatCTabFolderor another SWT container measures on every layout pass.CTabFolder.setTopRight(Composite)is the canonical case —CTabFolder.onResize/ layout pass →topRight.computeSize→GridLayout.layout→ToolBar.computeSize→ toxic toggle → invalidation → next frame → back toCTabFolder.onResize. The cycle repeats at vsync rate.Evidence —
bpftraceuprobe traceAttached uprobes on
gdk_window_invalidate_rect/gdk_window_invalidate_regionin a running NetXMS management console (one of many SWT applications that usesCTabFolderwith aSWT.WRAPtop-right toolbar). 10-second capture:2344 invalidations in 10 s = ~234 per second, all from
Java_org_eclipse_swt_internal_gtk3_GTK3_gtk_1toolbar_1set_1show_1arrow. No other callers appear in the profile.Evidence — instrumented
CTabFolderRendererSubstituting a
CTabFolderRenderersubclass that countsdraw(PART_HEADER)calls and logs a Java stack trace every N calls produced this sample log line (NetXMS console, 9-tab perspective):59.5 paints per second — vsync. No Java application code appears above
draw(); every paint enters throughgtk_main_do_event→Display.readAndDispatch. The application is not callingredraw(),layout(), or anything else — the entire loop runs below the Java layer and re-enters SWT only for rendering.Proposed fixes
In approximate order of invasiveness — any one of these would break the loop:
1. Cache the computed size and avoid redundant toggles. The simplest fix.
ToolBar.computeSizeInPixelscan cache the most recent(wHint, hHint, state)→sizeresult and return it without calling native measurement if neither the items nor the hints have changed. Achanged == trueargument would invalidate the cache. This also benefits Win32 / Cocoa code paths where native measurement has a non-trivial cost.2. Guard the
set_show_arrow(true)re-enable with a state check. Track SWT's "desiredshow_arrowstate" in a Java field; only callgtk_toolbar_set_show_arrow(handle, true)if SWT's field says the widget should currently haveshow_arrow = trueand the previous call flipped it off. Avoid unconditionally re-enabling it after every measurement. This is the smallest diff but doesn't address the firstset_show_arrow(false)call when the previous state was alreadyfalse(which is the no-op case in GTK and costs nothing anyway).3. Replace the toggle workaround with a direct
gtk_widget_get_preferred_width/_heightmeasurement. The comment inToolBar.computeSizeInPixelsexplains thatset_show_arrow(false)works around a GTK sizing bug. GTK3's preferred-size API has been stable since ~3.10 and the original workaround may no longer be necessary. Ifgtk_widget_get_preferred_sizereturns the correct dimensions for a WRAP toolbar without theshow_arrowdance, the entire toggle path can be removed on GTK3.4. Document the gotcha. If none of the above are accepted, at minimum
CTabFolder.setTopRightandToolBarshould document that placing aSWT.WRAPtoolbar in a frequently-measured parent on GTK3 will cause a repaint loop. Every SWT-on-GTK3 application is currently affected without knowing it.Downstream workaround
Applications that cannot wait for an SWT release can work around the bug by not using
SWT.WRAPon anyToolBarplaced insideCTabFolder.setTopRight(...)or any similarly-measured parent. The cost is the loss of the overflow chevron; items that don't fit are clipped instead of hidden behind a dropdown. In practice most SWT-based UIs have ≤ 5 items in their top-right toolbar and fit trivially, so the visible regression is nil.Impact estimate
Any SWT-on-GTK3 application that uses
CTabFolder.setTopRight(Composite)with aSWT.WRAP-styledToolBarchild — which is the idiomatic pattern in Eclipse IDE, NetXMS nxmc, and many JFace-based applications — is silently burning 10–25 % of one CPU core per open tab folder. The cost is not visible in Java Flight Recorder / YourKit / JProfiler because the loop originates in GTK and re-enters Java only through the SWT event loop; standard Java profiling reports "the main thread is inDisplay.sleep()/gtk_main_iteration_do" and does not flag the per-frame measurement cost.Users experience this as battery drain on laptops, fans running on workstations, and the SWT main thread being less responsive under load. The root cause has been dormant since 2012 and is unlikely to be discovered without tracing at the GDK level.
ToolBarWrapRepaintLoop.java