Skip to content

[GTK] Speed up Combo setItems/removeAll/remove for large item counts - #13

Closed
vogella wants to merge 4 commits into
masterfrom
claude/happy-newton-thx71f
Closed

[GTK] Speed up Combo setItems/removeAll/remove for large item counts#13
vogella wants to merge 4 commits into
masterfrom
claude/happy-newton-thx71f

Conversation

@vogella

@vogella vogella commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Problem

Setting or removing a large number of combo items (>5000) is very slow on GTK
(see eclipse-platform#506 — ~2.2s to add 5000 items,
~4s to clear them).

The bottleneck is not the storage: GtkComboBox recomputes its popup/cell-view
layout on every single model change while the model is attached, so each
inserted/removed row triggers a full relayout — O(n²)/O(n)-per-row behaviour.
(This is a different bottleneck from the older wrap-width regression in bug
489640, which was already mitigated with delayed wrap.)

Fix

Use the standard GTK idiom of detaching the GtkListStore model during bulk
updates so the widget reacts only once:

  1. g_object_ref the model, then gtk_combo_box_set_model(handle, 0) to detach.
  2. Bulk-modify the store directly while detached.
  3. Re-attach the model and g_object_unref.

Applied to all three bulk operations:

Method Before After
setItems(...) per-item gtk_combo_box_text_insert direct gtk_list_store_insert/set while detached
removeAll() gtk_combo_box_text_remove_all (reacts per row) gtk_list_store_clear while detached
remove(start, end) loop of gtk_combo_box_text_remove direct gtk_list_store_remove (end→start) while detached

Adds one native binding, gtk_combo_box_set_model, in GTK.java. Both GTK3 and
GTK4 use a GtkTreeModel, so the same path works for both. Existing behaviour
(selection clearing, RTL handling, delayed wrap) is preserved. The direct-insert
path uses column 0 for text, matching the cell-renderer attribute already
hardcoded in createHandle.

remove(int index) (single item) is intentionally left as-is — detaching for one
row isn't worthwhile.

Notes / testing

  • A new native method means os.c must be regenerated and the native library
    rebuilt; this happens in the GitHub CI build (natives are rebuilt each run),
    so this change needs the Linux GTK3/GTK4 CI run to validate linking and
    behaviour. It could not be compiled/tested locally.
  • Adding the performance label would let CI run the timing tests.

Fixes eclipse-platform#506

🤖 Generated with Claude Code


Generated by Claude Code

claude added 2 commits June 22, 2026 17:21
Setting or removing a large number of combo items (>5000) was very slow on
GTK because GtkComboBox recomputes its popup/cell-view layout on every single
model change, leading to O(n^2)/O(n) per-row overhead.

Detach the GtkListStore model from the combo box before bulk modifying it and
re-attach it afterwards, so the widget reacts only once. setItems now also
populates the GtkListStore directly instead of going through the per-item
GtkComboBoxText convenience function.

Adds the gtk_combo_box_set_model native binding.

Fixes eclipse-platform#506

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Suf4gTtBDjKBbFBzJd1mVr
Apply the same model-detach optimization as setItems/removeAll to the
range-remove path: detach the GtkListStore, remove the rows directly from
the store (from the end downwards to keep indices stable), then re-attach
so GtkComboBox recomputes its layout only once instead of per removed row.

Refs eclipse-platform#506

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Suf4gTtBDjKBbFBzJd1mVr

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes bulk item operations in Combo.java (such as remove, removeAll, and setItems) by temporarily detaching the underlying GtkTreeModel from the GtkComboBox, performing the modifications directly on the GtkListStore, and then re-attaching the model. This prevents the widget from updating on every single change, resolving performance issues. Feedback on these changes includes a critical bug fix to restore the active selection index, which is lost when the model is detached, and an optimization to reuse a single GtkTreeIter allocation during bulk insertions instead of allocating it per item.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 2068 to 2093
int index = GTK.gtk_combo_box_get_active (handle);
if (start <= index && index <= end) clearText();

gtk_combo_box_toggle_wrap(false);
for (int i = end; i >= start; i--) {
if (handle != 0) GTK.gtk_combo_box_text_remove(handle, i);
long model = handle != 0 ? GTK.gtk_combo_box_get_model (handle) : 0;
if (model != 0) {
/*
* Bug 506: Removing a range of items one-by-one is slow because the
* GtkComboBox reacts to every single row deletion. Detach the model,
* remove the rows directly from the GtkListStore (from the end of the
* range downwards so indices stay stable), and re-attach it so the
* widget only updates once.
*/
OS.g_object_ref (model);
gtk_combo_box_toggle_wrap (false);
GTK.gtk_combo_box_set_model (handle, 0);
long iter = OS.g_malloc (GTK.GtkTreeIter_sizeof ());
for (int i = end; i >= start; i--) {
if (GTK.gtk_tree_model_iter_nth_child (model, iter, 0, i)) {
GTK.gtk_list_store_remove (model, iter);
}
}
OS.g_free (iter);
GTK.gtk_combo_box_set_model (handle, model);
OS.g_object_unref (model);
gtk_combo_box_toggle_wrap (true);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When the model is detached from the GtkComboBox using GTK.gtk_combo_box_set_model(handle, 0), the active selection index is reset to -1 (none) by GTK. When the model is re-attached, the selection is not automatically restored, resulting in a loss of the user's selection if the active item was not in the removed range.

To fix this regression, we should calculate the adjusted active index before detaching the model, and restore it (while blocking the CHANGED signal to avoid redundant events) after re-attaching the model.

	int index = GTK.gtk_combo_box_get_active (handle);
	if (start <= index && index <= end) clearText();
	int newIndex = -1;
	if (index != -1 && !(start <= index && index <= end)) {
		newIndex = index > end ? index - (end - start + 1) : index;
	}

	long model = handle != 0 ? GTK.gtk_combo_box_get_model (handle) : 0;
	if (model != 0) {
		/*
		 * Bug 506: Removing a range of items one-by-one is slow because the
		 * GtkComboBox reacts to every single row deletion. Detach the model,
		 * remove the rows directly from the GtkListStore (from the end of the
		 * range downwards so indices stay stable), and re-attach it so the
		 * widget only updates once.
		 */
		OS.g_object_ref (model);
		gtk_combo_box_toggle_wrap (false);
		GTK.gtk_combo_box_set_model (handle, 0);
		long iter = OS.g_malloc (GTK.GtkTreeIter_sizeof ());
		for (int i = end; i >= start; i--) {
			if (GTK.gtk_tree_model_iter_nth_child (model, iter, 0, i)) {
				GTK.gtk_list_store_remove (model, iter);
			}
		}
		OS.g_free (iter);
		GTK.gtk_combo_box_set_model (handle, model);
		if (newIndex != -1) {
			OS.g_signal_handlers_block_matched (handle, OS.G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, CHANGED);
			GTK.gtk_combo_box_set_active (handle, newIndex);
			OS.g_signal_handlers_unblock_matched (handle, OS.G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, CHANGED);
		}
		OS.g_object_unref (model);
		gtk_combo_box_toggle_wrap (true);
	}

Comment on lines +2448 to +2457
OS.g_object_ref (model);
gtk_combo_box_toggle_wrap (false);
GTK.gtk_combo_box_set_model (handle, 0);
GTK.gtk_list_store_clear (model);
for (int i = 0; i < items.length; i++) {
gtk_list_store_insert (model, items [i], i);
}
GTK.gtk_combo_box_set_model (handle, model);
OS.g_object_unref (model);
gtk_combo_box_toggle_wrap (true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To optimize bulk insertions, we can allocate the GtkTreeIter once outside the loop and reuse it, rather than allocating and freeing it for every single item inside gtk_list_store_insert. This significantly reduces native memory allocation overhead (g_malloc / g_free) when setting a large number of items.

		OS.g_object_ref (model);
		gtk_combo_box_toggle_wrap (false);
		GTK.gtk_combo_box_set_model (handle, 0);
		GTK.gtk_list_store_clear (model);
		long iter = OS.g_malloc (GTK.GtkTreeIter_sizeof ());
		for (int i = 0; i < items.length; i++) {
			gtk_list_store_insert (model, iter, items [i], i);
		}
		OS.g_free (iter);
		GTK.gtk_combo_box_set_model (handle, model);
		OS.g_object_unref (model);
		gtk_combo_box_toggle_wrap (true);

Comment on lines 2475 to 2481
private void gtk_list_store_insert (long model, String string, int index) {
byte[] buffer = Converter.wcsToMbcs (string, true);
long iter = OS.g_malloc (GTK.GtkTreeIter_sizeof ());
GTK.gtk_list_store_insert (model, iter, index);
GTK.gtk_list_store_set (model, iter, 0, buffer, -1);
OS.g_free (iter);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the signature of the private helper method gtk_list_store_insert to accept the pre-allocated iter pointer, avoiding per-item allocation overhead.

private void gtk_list_store_insert (long model, long iter, String string, int index) {
	byte[] buffer = Converter.wcsToMbcs (string, true);
	GTK.gtk_list_store_insert (model, iter, index);
	GTK.gtk_list_store_set (model, iter, 0, buffer, -1);
}

claude added 2 commits June 22, 2026 17:33
- remove(start, end): restore the active selection after the model is
  re-attached. Detaching the model resets the active item to -1, so a
  selection outside the removed range would otherwise be lost. The index is
  adjusted for the removed rows and restored with the CHANGED signal blocked
  to avoid a spurious Modify event, mirroring select(int).
- setItems: allocate the GtkTreeIter once and reuse it across the insert
  loop instead of allocating/freeing per item.

Refs eclipse-platform#506

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Suf4gTtBDjKBbFBzJd1mVr
The CI native build compiles the committed generated JNI glue rather than
regenerating it from the Java sources, so adding the gtk_combo_box_set_model
binding to GTK.java alone caused UnsatisfiedLinkError in the Combo tests.

Add the corresponding generated entries (os.c JNI function and the os_stats.h
function enum) so the native library exports the symbol. The function is
available on both GTK3 and GTK4.

Refs eclipse-platform#506

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Suf4gTtBDjKBbFBzJd1mVr
@github-actions

Copy link
Copy Markdown

Test Results (linux)

   94 files  ±0     94 suites  ±0   15m 20s ⏱️ +58s
4 597 tests ±0  4 373 ✅ ±0  224 💤 ±0  0 ❌ ±0 
3 383 runs  ±0  3 308 ✅ ±0   75 💤 ±0  0 ❌ ±0 

Results for commit ab7962a. ± Comparison against base commit 7f9089e.

@vogella

vogella commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Superseded by eclipse-platform#3401 (clean single commit, rebased on current master, shorter description).

@vogella vogella closed this Jun 24, 2026
@vogella
vogella deleted the claude/happy-newton-thx71f branch June 24, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setting/removing large number of (more than 5000) combo items is too slow

2 participants