[GTK3] ToolBar.computeSize with SWT.WRAP causes self-sustaining 60 Hz repaint loop in CTabFolder.setTopRight
## 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.
```java
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):
```java
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](https://github.com/user-attachments/files/26721063/ToolBarWrapRepaintLoop.java)
0 条评论