[GTK] Speed up Combo setItems/removeAll/remove for large item counts - #13
[GTK] Speed up Combo setItems/removeAll/remove for large item counts#13vogella wants to merge 4 commits into
Conversation
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
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| 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); |
There was a problem hiding this comment.
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);| 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); | ||
| } |
There was a problem hiding this comment.
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);
}- 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
|
Superseded by eclipse-platform#3401 (clean single commit, rebased on current master, shorter description). |
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:
GtkComboBoxrecomputes its popup/cell-viewlayout 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
GtkListStoremodel during bulkupdates so the widget reacts only once:
g_object_refthe model, thengtk_combo_box_set_model(handle, 0)to detach.g_object_unref.Applied to all three bulk operations:
setItems(...)gtk_combo_box_text_insertgtk_list_store_insert/setwhile detachedremoveAll()gtk_combo_box_text_remove_all(reacts per row)gtk_list_store_clearwhile detachedremove(start, end)gtk_combo_box_text_removegtk_list_store_remove(end→start) while detachedAdds one native binding,
gtk_combo_box_set_model, inGTK.java. Both GTK3 andGTK4 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 onerow isn't worthwhile.
Notes / testing
os.cmust be regenerated and the native libraryrebuilt; 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.
performancelabel would let CI run the timing tests.Fixes eclipse-platform#506
🤖 Generated with Claude Code
Generated by Claude Code