Skip to content

Commit 13fa66d

Browse files
committed
perf(gui): yield to Tk event loop during table/component rendering
- Yield every 20 rendered rows in the parameter editor to keep the UI responsive during large table builds - Yield every 5 components in the component editor for the same reason
1 parent f25a76a commit 13fa66d

4 files changed

Lines changed: 52 additions & 1 deletion

ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,11 @@ def set_combobox_entries_preserving_width(
350350
def populate_frames(self) -> None:
351351
"""Populates the ScrollFrame with widgets based on the JSON data."""
352352
components = self.data_model.get_all_components()
353-
for key, value in components.items():
353+
for i, (key, value) in enumerate(components.items(), 1):
354354
self.add_widget(self.scroll_frame.view_port, key, value, [])
355+
if i % 5 == 0: # yield to the event loop periodically to keep the UI responsive
356+
self.scroll_frame.view_port.update_idletasks()
357+
355358
# Scroll to the top after (re-)populating
356359
self.scroll_frame.canvas.yview("moveto", 0)
357360

ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ def _update_table(self, params: dict[str, ArduPilotParameter], gui_complexity: s
232232
if self.parameter_editor.should_display_bitmask_parameter_editor_usage(param_name):
233233
should_try_to_display_bitmask_parameter_editor_usage = True
234234
self._grid_column_widgets(row_widgets, i, show_upload_column)
235+
if i % 20 == 0:
236+
self.view_port.update_idletasks() # yield to the event loop periodically to keep the UI responsive
235237

236238
# Add the "Add" button at the bottom of the table
237239
add_button = ttk.Button(self.view_port, text=_("Add"), style="narrow.TButton", command=self._on_parameter_add)

tests/test_frontend_tkinter_component_editor_base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,25 @@ def test_user_can_interact_with_different_widget_types(
777777
# Assert: Widget addition should be called for both cases
778778
assert editor_with_realistic_data._add_widget.call_count == 2
779779

780+
def test_populate_frames_yields_to_event_loop_every_five_components(
781+
self, editor_with_realistic_data: ComponentEditorWindowBase
782+
) -> None:
783+
"""
784+
populate_frames yields to the Tk event loop every fifth component to keep the UI responsive.
785+
786+
GIVEN: An editor with more than five components to display
787+
WHEN: populate_frames is called
788+
THEN: update_idletasks is called once per complete group of five components
789+
"""
790+
num_components = 15 # expect floor(15 / 5) == 3 yields
791+
components = {f"Component_{i}": {"value": i} for i in range(num_components)}
792+
editor_with_realistic_data.data_model.get_all_components.return_value = components
793+
794+
editor_with_realistic_data.populate_frames()
795+
796+
expected_yields = num_components // 5
797+
assert editor_with_realistic_data.scroll_frame.view_port.update_idletasks.call_count == expected_yields
798+
780799

781800
class TestComplexityComboboxWorkflows:
782801
"""Test user workflows for GUI complexity management."""

tests/test_frontend_tkinter_parameter_editor_table.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,6 +1703,33 @@ def test_update_table_creates_add_button_with_tooltip(self, parameter_editor_tab
17031703
tooltip_call_args = mock_tooltip.call_args[0]
17041704
assert "Add a parameter to the test_file.param file" in tooltip_call_args[1]
17051705

1706+
def test_update_table_yields_to_event_loop_every_twenty_rows(self, parameter_editor_table) -> None:
1707+
"""
1708+
Table update yields to the event loop periodically during large renders.
1709+
1710+
GIVEN: More than twenty parameters to render
1711+
WHEN: The table is updated
1712+
THEN: The viewport idletasks are processed every twentieth row
1713+
"""
1714+
params = {
1715+
f"TEST_PARAM_{index:02d}": create_mock_data_model_ardupilot_parameter(
1716+
name=f"TEST_PARAM_{index:02d}", value=float(index)
1717+
)
1718+
for index in range(1, 46)
1719+
}
1720+
1721+
with (
1722+
patch.object(parameter_editor_table, "_create_column_widgets", return_value=[MagicMock() for _ in range(7)]),
1723+
patch.object(parameter_editor_table, "_grid_column_widgets"),
1724+
patch.object(parameter_editor_table, "_configure_table_columns"),
1725+
patch.object(parameter_editor_table.view_port, "update_idletasks") as mock_update_idletasks,
1726+
patch("tkinter.ttk.Button"),
1727+
patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.show_tooltip"),
1728+
):
1729+
parameter_editor_table._update_table(params, "simple")
1730+
1731+
assert mock_update_idletasks.call_count == 2
1732+
17061733
def test_create_flightcontroller_value_sets_correct_background_colors(self, parameter_editor_table) -> None: # pylint: disable=too-many-statements # noqa: PLR0915
17071734
"""
17081735
Flight controller value labels display with appropriate background colors based on parameter state.

0 commit comments

Comments
 (0)