From 56060ff936c0240abdab48580759ac8552be1ed9 Mon Sep 17 00:00:00 2001 From: Rene Hopf Date: Wed, 4 Nov 2026 23:48:14 +0100 Subject: [PATCH 1/3] gmoccapy: only set labels whose text changed GTK relayouts the whole window on every GtkLabel set_label()/set_text(), even when the text is the same. gmoccapy rewrote the feed, velocity, spindle and Vc labels and the clock every 100 ms cycle, and Combi_DRO its three readouts on every position update, so an idle gmoccapy redrew its window 10 times a second. Only set a label when its text changed, and update the clock from the 1 s timer, since it only shows seconds. Idle 4-axis sim: CPU from ~10.5% to ~7% of one core, window redraws from 10/s to 1/s. --- lib/python/gladevcp/combi_dro.py | 9 ++++-- src/emc/usr_intf/gmoccapy/gmoccapy.py | 40 +++++++++++++++++---------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/python/gladevcp/combi_dro.py b/lib/python/gladevcp/combi_dro.py index 8a5592aceab..e77cea6c42f 100644 --- a/lib/python/gladevcp/combi_dro.py +++ b/lib/python/gladevcp/combi_dro.py @@ -495,9 +495,12 @@ def _position(self, object, p, rel_p, dtg, joint_actual_position): main_dro = tmpl(main * scale) left_dro = tmpl(left * scale) right_dro = tmpl(right * scale) - self.widgets["main_dro"].set_label(main_dro) - self.widgets["dro_left"].set_label(left_dro) - self.widgets["dro_right"].set_label(right_dro) + # GTK relayouts the whole window on every set_label(), even for the + # same text, so only touch the labels whose text changed + for name, text in (("main_dro", main_dro), ("dro_left", left_dro), ("dro_right", right_dro)): + label = self.widgets[name] + if label.get_label() != text: + label.set_label(text) self.dtg = dtg * scale self.abs_pos = abs_pos * scale self.rel_pos = rel_pos * scale diff --git a/src/emc/usr_intf/gmoccapy/gmoccapy.py b/src/emc/usr_intf/gmoccapy/gmoccapy.py index cf17081c331..b31d5d3892b 100644 --- a/src/emc/usr_intf/gmoccapy/gmoccapy.py +++ b/src/emc/usr_intf/gmoccapy/gmoccapy.py @@ -167,6 +167,15 @@ def excepthook(exc_type, exc_obj, exc_tb): AXISLIST = ['offset', 'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'Rot', 'name'] +def set_label(label, text): + """Set a label's text only if it changed. + + GTK relayouts the whole window on every set_label(), even for the same + text, which costs a lot of CPU when done every cycle. + """ + if label.get_label() != text: + label.set_label(text) + class gmoccapy(object): def __init__(self, argv): @@ -552,7 +561,8 @@ def __init__(self, argv): # CYCLE_TIME = time, in milliseconds, that display will sleep between polls cycle_time = self.get_ini_info.get_cycle_time() GLib.timeout_add( cycle_time, self._periodic ) # time between calls to the function, in milliseconds - GLib.timeout_add(1000, self._periodic_1s) # 1 s timer for elapsed time display + GLib.timeout_add(1000, self._periodic_1s) # 1 s timer for elapsed time and clock display + self._update_clock() self.elapsed_time_run = 0 self.progress = 0 @@ -2815,19 +2825,21 @@ def _periodic(self): self._update_halui_pin() self._update_vc() - self.widgets.lbl_time.set_label(strftime("%H:%M:%S") + "\n" + strftime("%d.%m.%Y")) - # keep the timer running return True # every 1 second this gets called def _periodic_1s(self): + self._update_clock() if self.GSTAT.is_auto_running() and not self.GSTAT.is_auto_paused(): self.elapsed_time_run += 1 self._update_progress() return True + def _update_clock(self): + self.widgets.lbl_time.set_label(strftime("%H:%M:%S") + "\n" + strftime("%d.%m.%Y")) + def _show_error(self, error): kind, text = error if kind in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): @@ -3937,7 +3949,7 @@ def _update_vel(self): # self.stat.program_units will return 1 for inch, 2 for mm and 3 for cm real_feed = float(self.stat.settings[1] * self.stat.feedrate) if self.stat.program_units != 1: - self.widgets.lbl_current_vel.set_text("{0:d}".format(int(self.stat.current_vel * 60.0 * self.faktor))) + set_label(self.widgets.lbl_current_vel, "{0:d}".format(int(self.stat.current_vel * 60.0 * self.faktor))) if "G95" in self.active_gcodes: feed_str = "{0:d}".format(int(self.stat.settings[1])) real_feed_str = "F {0:.2f}".format(real_feed) @@ -3945,7 +3957,7 @@ def _update_vel(self): feed_str = "{0:d}".format(int(self.stat.settings[1])) real_feed_str = "F {0:d}".format(int(real_feed)) else: - self.widgets.lbl_current_vel.set_text("{0:.2f}".format(self.stat.current_vel * 60.0 * self.faktor)) + set_label(self.widgets.lbl_current_vel, "{0:.2f}".format(self.stat.current_vel * 60.0 * self.faktor)) if "G95" in self.active_gcodes: feed_str = "{0:.4f}".format(self.stat.settings[1]) real_feed_str = "F {0:.4f}".format(real_feed) @@ -3958,8 +3970,8 @@ def _update_vel(self): if not real_feed: real_feed_str = "F 0" - self.widgets.lbl_active_feed.set_label(feed_str) - self.widgets.lbl_feed_act.set_text(real_feed_str) + set_label(self.widgets.lbl_active_feed, feed_str) + set_label(self.widgets.lbl_feed_act, real_feed_str) def _update_coolant(self): if self.stat.flood: @@ -4506,18 +4518,18 @@ def _update_spindle(self): return # set the S label in active code frame - self.widgets.active_speed_label.set_label("{0:.0f}".format(self.stat.settings[2])) + set_label(self.widgets.active_speed_label, "{0:.0f}".format(self.stat.settings[2])) # set the 'Spindle [rpm]' label #Note: self.stat.spindle[0]['speed'] does not reflect 'spindle.0.speed-out' pins when using G96 mode (issue #3449) speed = hal.get_value("spindle.0.speed-out") # catch very large values eg when using G96 w/o D value if speed > self.max_spindle_disp: - self.widgets.lbl_spindle_act.set_text(">"+str(self.max_spindle_disp)) + set_label(self.widgets.lbl_spindle_act, ">"+str(self.max_spindle_disp)) elif speed < -self.max_spindle_disp: - self.widgets.lbl_spindle_act.set_text("<"+str(self.max_spindle_disp)) + set_label(self.widgets.lbl_spindle_act, "<"+str(self.max_spindle_disp)) else: - self.widgets.lbl_spindle_act.set_text("{0}".format(int(round(speed)))) + set_label(self.widgets.lbl_spindle_act, "{0}".format(int(round(speed)))) def _update_vc(self): #Note: self.stat.spindle[0]['speed'] does not reflect 'spindle.0.speed-out' pins when using G96 mode (issue #3449) @@ -4537,7 +4549,7 @@ def _update_vc(self): text = "Vc= {0:2.1f}".format(vc) else: text = "Vc= {0:.2f}".format(vc) - self.widgets.lbl_vc.set_text(text) + set_label(self.widgets.lbl_vc, text) def on_program_time(self, widget, estimate): """The parse's time estimate for the program just loaded. @@ -4648,13 +4660,13 @@ def _set_spindle(self, command): rpm_out = rpm / self.stat.spindle[0]['override'] except: rpm_out = 0 - self.widgets.lbl_spindle_act.set_label("{0}".format(int(rpm))) + set_label(self.widgets.lbl_spindle_act, "{0}".format(int(rpm))) if command == "stop": # documentation of self.command.spindle() # linuxcnc.spindle(direction, speed, spindle=0) self.command.spindle(0) - self.widgets.lbl_spindle_act.set_label("0") + set_label(self.widgets.lbl_spindle_act, "0") elif command == "forward": self.command.spindle(1, rpm_out) elif command == "reverse": From 35f47266db0bcc31cdeb02fcc262324dec490c0a Mon Sep 17 00:00:00 2001 From: Rene Hopf Date: Wed, 4 Nov 2026 23:48:31 +0100 Subject: [PATCH 2/3] gremlin: don't add a poll timer every time the preview is shown Gremlin.map() started a new 50 ms poll timer each time the widget was mapped, and poll() never stops, so every hide and show of the preview (eg opening and leaving gmoccapy's settings page) left one more timer polling linuxcnc status for the rest of the session. Keep the timer id, start it only if none is running, and stop it on unmap since there is nothing to draw while hidden. --- src/emc/usr_intf/gremlin/gremlin.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/emc/usr_intf/gremlin/gremlin.py b/src/emc/usr_intf/gremlin/gremlin.py index 84d31c7b5d7..4bb214d0545 100755 --- a/src/emc/usr_intf/gremlin/gremlin.py +++ b/src/emc/usr_intf/gremlin/gremlin.py @@ -133,6 +133,7 @@ def C(s): self.connect('resize', self.reshape) # 'map', not 'map-event': a GLArea has no window of its own to get one. self.connect('map', self.map) + self.connect('unmap', self.unmap) self.connect('render', self.render) self.connect('motion-notify-event', self.motion) self.connect('button-press-event', self.pressed) @@ -151,6 +152,7 @@ def C(s): self.fingerprint = () + self.poll_timer = None self.lat = 0 self.minlat = -90 @@ -293,15 +295,25 @@ def _redraw(self): def clear_live_plotter(self): self.logger.clear() + # 'map' fires every time the widget is shown again, eg when gmoccapy + # leaves its settings page, so only start the poll if it is not running. def map(self, *args): - GLib.timeout_add(50, self.poll) + if self.poll_timer is None: + self.poll_timer = GLib.timeout_add(50, self.poll) + + # Nothing to draw while hidden, so stop polling. + def unmap(self, *args): + if self.poll_timer is not None: + GLib.source_remove(self.poll_timer) + self.poll_timer = None def poll(self): s = self.stat try: s.poll() except: - return + self.poll_timer = None + return False fingerprint = (self.logger.npts, self.soft_limits(), s.actual_position, s.joint_actual_position, s.homed, s.g5x_offset, s.g92_offset, s.limit, s.tool_in_spindle, @@ -311,7 +323,6 @@ def poll(self): self.fingerprint = fingerprint self.queue_render() - # return self.visible return True @rs274.glcanon.with_context From dcb85464ec4d601736690fb3b9ec361893209be5 Mon Sep 17 00:00:00 2001 From: Rene Hopf Date: Wed, 4 Nov 2026 23:48:31 +0100 Subject: [PATCH 3/3] gmoccapy: stop the warning dialog's pin timer when it closes warning_dialog() watches the confirm and active pins with a 100 ms timer that only ends when a pin closes the dialog. Closing it with the Ok button left the timer running forever, so every operator warning leaked a timer, which later tried to close a destroyed dialog when the pin went high. Remove the timer after the dialog returns, unless a pin already ended it. --- src/emc/usr_intf/gmoccapy/dialogs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/emc/usr_intf/gmoccapy/dialogs.py b/src/emc/usr_intf/gmoccapy/dialogs.py index 842df6b2a08..2d00309f4f5 100644 --- a/src/emc/usr_intf/gmoccapy/dialogs.py +++ b/src/emc/usr_intf/gmoccapy/dialogs.py @@ -125,18 +125,25 @@ def warning_dialog(self, caller, message, secondary = None, title = _("Operator self.emit("play_sound", "alert") dialog.set_title(title) + timer = None def periodic(): + nonlocal timer if caller.halcomp[confirm_pin]: + timer = None dialog.response(Gtk.ResponseType.OK) return False if active_pin is not None: if not caller.halcomp[active_pin]: + timer = None dialog.response(Gtk.ResponseType.CANCEL) return False return True - GLib.timeout_add(100, periodic) + timer = GLib.timeout_add(100, periodic) response = dialog.run() + # closed with the button: stop watching the pins + if timer is not None: + GLib.source_remove(timer) dialog.destroy() return response == Gtk.ResponseType.OK