Skip to content

[GTK3] ToolBar.computeSize with SWT.WRAP causes self-sustaining 60 Hz repaint loop in CTabFolder.setTopRight #3236

@zev333

Description

@zev333

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 settop -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.WRAPmain 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.computeSizeGridLayout.layoutToolBar.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_eventDisplay.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions