A batch of SWT performance fixes and cleanups

4 minute read

A while ago we went through the core classes of SWT looking for hot spots and dead weight for our custom Eclipse IDE build, which we and some of our clients use to have a faster Eclipse IDE. The candidates came from an AI-assisted review, but none of them was taken on trust: every claim was checked against the code and benchmarked where possible. Several ideas did not survive that check, for example savings of about 70 ns per mouse motion event that are not worth the extra code. After using them for a while, we contributed them upstream to eclipse.platform.swt, and all of them are now merged into SWT master.

The biggest wins

Clearing a SWT.MULTI List with 100,000 entries took almost a minute on GTK and now takes under a second, which also speeds up JFace ListViewer.setInput and refresh. Disposing tree items one by one, as JFace AbstractTreeViewer does on remove and refresh, no longer walks all root rows on every call, so 50,000 items go away in about 0.15 s instead of 8 to 10 s. On GTK, Tree.getItemCount() and TreeItem.getItemCount() are now cached instead of walking the sibling list on every call, so 40,000 calls on an item with 40,000 children take about 1 ms instead of 7 to 12 s. GridLayout grows its grid geometrically instead of four rows at a time, which turns a quadratic computeSize into a linear one for composites with many children. List.setSelection(String[]) stops copying the whole list natively for every string.

How it was measured

All numbers are from Linux with GTK 3.24 on X11 under Xvfb, Java 21 and 25, comparing the old and the new class on the same build on a single machine. Ranges show run-to-run variation. Most hot spots are GTK specific; #3640, #3642, #3646, #3649, #3650 and #3654 are platform independent or touch all platforms.

Performance changes

PR Change Scenario Before After
#3655 [GTK] List.removeAll/setItems on SWT.MULTI lists switch to browse selection mode around gtk_list_store_clear, as Table.removeAll does removeAll, 20k / 50k / 100k rows 1.29 s / 10.05 s / 54.8 s 0.19 s / 0.46 s / 0.98 s
    setItems (one item), 20k / 50k / 100k rows 1.26 s / 10.0 s / 55.9 s 0.19 s / 0.48 s / 0.97 s
#3643 [GTK] Tree counts root rows for EmptinessChanged only when observed, then with an O(1) iterator check Insert at index 0, 40k / 160k items 3.3 s / 148 s 0.15 s / 0.59 s
    Append, 10k / 50k items 0.46-0.71 s / 17-22 s 0.34-0.41 s / about 14.5 s (rest is GtkTreeStore itself)
#3656 [GTK] TreeItem.dispose drops an always-true getItemCount() > 0 check that walked all root rows Dispose 50k root items from the front 8.0-10.6 s 4.7-6.5 s alone, 0.12-0.15 s with #3643
    Dispose 10k root items, with #3643 about 180 ms 31-45 ms
#3664 [GTK] Tree and TreeItem cache their child counts behind a counter bumped on every row insertion or removal, instead of calling gtk_tree_model_iter_n_children; a partial fix for #882, as getItem(int) and indexOf still use linear GTK lookups 40k getItemCount() calls, item with 40k children 7-12 s about 1 ms
    Reveal-like loop of getItemCount() plus getItem(i) baseline 3 to 5 times faster
#3642 GridLayout grows its grid geometrically and allocates rows lazily computeSize, 1,000 labels 4.4-7.1 ms, 3.4 MB allocated 0.27-0.40 ms, 44 KB
    computeSize, 5,000 labels 70-100 ms, 85.7 MB allocated 1.0-1.3 ms, 245 KB
    layout(), 5,000 labels about 100-130 ms 46-52 ms
#3657 [GTK] List.setSelection(String[]) fetches the items once; also fixes indexOf(String, int) throwing for a negative start 100 strings, 10k / 50k items 587 ms / 3.17 s 7 ms / 33 ms
#3640 Control.sort(int[]) uses Arrays.sort instead of a shell sort that was always O(n²); used by Table.remove(int[]) and List.remove(int[]) 50,000 indices, ascending / random 2,416 ms / 2,598 ms 2.5 ms / 10.7 ms
#3650 StyledText Bullet.indexOf uses binary search indexOf over all lines, 100k-line document 0.6-1.2 s 4-15 ms
    Full measurement, numbered bullet on every line 3.8-5.0 s 1.7-2.5 s (rest is text layout)
#3654 StyledTextRenderer.reset(int,int) loops over the range instead of building a boxed TreeSet, plus a condition fix setFont, 100k / 1M lines 17.3 ms / 187.6 ms 6.4 ms / 4.6 ms
    setTabs, 100k / 1M lines 12.9 ms / 198.6 ms 2.0 ms / 3.6 ms
#3641 [GTK] Display.getClosure reuses signal closures again, a guard lost in 2019 Widget creation about 20 new GClosures per control a new closure every 255 connections, roughly 80 JNI calls and native allocations less per widget
#3651 [GTK] Combo removes itself from its parent’s fixClipMap on dispose; also fixes clipping after setParent 200 Combos disposed 200 leaked entries with listeners and data 0
#3645 [GTK] Cheap Java checks before OS.isWayland() in sendMouseEvent Mouse events without listener up to 6 JNI calls per event none
#3652 [GTK] Control.gtk_draw reads the cairo clip rectangle only when a Paint listener exists Each draw of a control without Paint listener 1 allocation and 1 JNI call none, about 70 ns saved

Code simplification

Alongside the performance work, a handful of pull requests remove about 245 lines without changing behaviour. #3648 drops unused internal members on GTK (-95 lines). #3653 shares the cell renderer lookups of Table, Tree and List in Scrollable (-51 lines). #3646 simplifies the CTabFolder gradient setters with Arrays utilities, checked equivalent on 4 million random inputs (-42 lines). #3649 replaces 28 hand-written array copies with Arrays.copyOf and clone (-32 lines). #3647 extracts the search column update that was copied three times in Table and Tree (-18 lines), and #3654 above removes another 7.

Interested?

If you are also looking to improve your code base, have a look at our AI-based services.

Updated: