From d22b3fcbefa5dc73a5b42515a365188092c22f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Boisselier?= Date: Mon, 3 Jun 2024 17:49:28 +0200 Subject: [PATCH] switched to multiprocessing instead of threading --- shaketune/measurement/axes_input_shaper.py | 10 ++-- shaketune/measurement/axes_map.py | 8 +-- shaketune/measurement/belts_comparison.py | 8 +-- shaketune/measurement/vibrations_profile.py | 8 +-- shaketune/shaketune.py | 18 +++--- ...aketune_thread.py => shaketune_process.py} | 59 +++++++++++-------- 6 files changed, 59 insertions(+), 52 deletions(-) rename shaketune/{shaketune_thread.py => shaketune_process.py} (54%) diff --git a/shaketune/measurement/axes_input_shaper.py b/shaketune/measurement/axes_input_shaper.py index 14ac2b4..c4d77f1 100644 --- a/shaketune/measurement/axes_input_shaper.py +++ b/shaketune/measurement/axes_input_shaper.py @@ -3,12 +3,12 @@ from ..helpers.common_func import AXIS_CONFIG from ..helpers.console_output import ConsoleOutput -from ..shaketune_thread import ShakeTuneThread +from ..shaketune_process import ShakeTuneProcess from .accelerometer import Accelerometer from .resonance_test import vibrate_axis -def axes_shaper_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None: +def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: min_freq = gcmd.get_float('FREQ_START', default=5, minval=1) max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1) hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1) @@ -60,7 +60,7 @@ def axes_shaper_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None: toolhead.dwell(0.5) # Configure the graph creator - creator = st_thread.get_graph_creator() + creator = st_process.get_graph_creator() creator.configure(scv, max_sm, accel_per_hz) # set the needed acceleration values for the test @@ -95,8 +95,8 @@ def axes_shaper_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None: # And finally generate the graph for each measured axis ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...') ConsoleOutput.print('This may take some time (1-3min)') - st_thread.run() - st_thread.wait_for_completion() + st_process.run() + st_process.wait_for_completion() # Re-enable the input shaper if it was active if input_shaper is not None: diff --git a/shaketune/measurement/axes_map.py b/shaketune/measurement/axes_map.py index d3002f0..8660abb 100644 --- a/shaketune/measurement/axes_map.py +++ b/shaketune/measurement/axes_map.py @@ -2,11 +2,11 @@ from ..helpers.console_output import ConsoleOutput -from ..shaketune_thread import ShakeTuneThread +from ..shaketune_process import ShakeTuneProcess from .accelerometer import Accelerometer -def axes_map_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None: +def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: z_height = gcmd.get_float('Z_HEIGHT', default=20.0) speed = gcmd.get_float('SPEED', default=80.0, minval=20.0) accel = gcmd.get_int('ACCEL', default=1500, minval=100) @@ -70,6 +70,6 @@ def axes_map_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None: # Run post-processing ConsoleOutput.print('Analysis of the movements...') - creator = st_thread.get_graph_creator() + creator = st_process.get_graph_creator() creator.configure(accel) - st_thread.run() + st_process.run() diff --git a/shaketune/measurement/belts_comparison.py b/shaketune/measurement/belts_comparison.py index 3c196fc..ef7a203 100644 --- a/shaketune/measurement/belts_comparison.py +++ b/shaketune/measurement/belts_comparison.py @@ -3,13 +3,13 @@ from ..helpers.common_func import AXIS_CONFIG from ..helpers.console_output import ConsoleOutput -from ..shaketune_thread import ShakeTuneThread +from ..shaketune_process import ShakeTuneProcess from .accelerometer import Accelerometer from .motorsconfigparser import MotorsConfigParser from .resonance_test import vibrate_axis -def compare_belts_responses(gcmd, config, st_thread: ShakeTuneThread) -> None: +def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None: min_freq = gcmd.get_float('FREQ_START', default=5.0, minval=1) max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1) hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1.0, minval=1) @@ -29,7 +29,7 @@ def compare_belts_responses(gcmd, config, st_thread: ShakeTuneThread) -> None: # Configure the graph creator motors_config_parser = MotorsConfigParser(config, motors=None) - creator = st_thread.get_graph_creator() + creator = st_process.get_graph_creator() creator.configure(motors_config_parser.kinematics, accel_per_hz) if motors_config_parser.kinematics == 'corexy': @@ -102,4 +102,4 @@ def compare_belts_responses(gcmd, config, st_thread: ShakeTuneThread) -> None: # Run post-processing ConsoleOutput.print('Belts comparative frequency profile generation...') ConsoleOutput.print('This may take some time (3-5min)') - st_thread.run() + st_process.run() diff --git a/shaketune/measurement/vibrations_profile.py b/shaketune/measurement/vibrations_profile.py index fcf40ff..625d887 100644 --- a/shaketune/measurement/vibrations_profile.py +++ b/shaketune/measurement/vibrations_profile.py @@ -4,14 +4,14 @@ import math from ..helpers.console_output import ConsoleOutput -from ..shaketune_thread import ShakeTuneThread +from ..shaketune_process import ShakeTuneProcess from .accelerometer import Accelerometer from .motorsconfigparser import MotorsConfigParser MIN_SPEED = 2 # mm/s -def create_vibrations_profile(gcmd, config, st_thread: ShakeTuneThread) -> None: +def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> None: size = gcmd.get_float('SIZE', default=100.0, minval=50.0) z_height = gcmd.get_float('Z_HEIGHT', default=20.0) max_speed = gcmd.get_float('MAX_SPEED', default=200.0, minval=10.0) @@ -127,6 +127,6 @@ def create_vibrations_profile(gcmd, config, st_thread: ShakeTuneThread) -> None: # Run post-processing ConsoleOutput.print('Machine vibrations profile generation...') ConsoleOutput.print('This may take some time (5-8min)') - creator = st_thread.get_graph_creator() + creator = st_process.get_graph_creator() creator.configure(motors_config_parser.kinematics, accel, motors_config_parser) - st_thread.run() + st_process.run() diff --git a/shaketune/shaketune.py b/shaketune/shaketune.py index 4be0955..93d26eb 100644 --- a/shaketune/shaketune.py +++ b/shaketune/shaketune.py @@ -14,7 +14,7 @@ from .measurement import ( ) from .post_processing import AxesMapFinder, BeltsGraphCreator, ShaperGraphCreator, VibrationsGraphCreator from .shaketune_config import ShakeTuneConfig -from .shaketune_thread import ShakeTuneThread +from .shaketune_process import ShakeTuneProcess class ShakeTune: @@ -108,23 +108,23 @@ class ShakeTune: def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None: ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') axes_map_finder = AxesMapFinder(self._config) - st_thread = ShakeTuneThread(self._config, axes_map_finder, self.timeout) - axes_map_calibration(gcmd, self._pconfig, st_thread) + st_process = ShakeTuneProcess(self._config, axes_map_finder, self.timeout) + axes_map_calibration(gcmd, self._pconfig, st_process) def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None: ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') belt_graph_creator = BeltsGraphCreator(self._config) - st_thread = ShakeTuneThread(self._config, belt_graph_creator, self.timeout) - compare_belts_responses(gcmd, self._pconfig, st_thread) + st_process = ShakeTuneProcess(self._config, belt_graph_creator, self.timeout) + compare_belts_responses(gcmd, self._pconfig, st_process) def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None: ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') shaper_graph_creator = ShaperGraphCreator(self._config) - st_thread = ShakeTuneThread(self._config, shaper_graph_creator, self.timeout) - axes_shaper_calibration(gcmd, self._pconfig, st_thread) + st_process = ShakeTuneProcess(self._config, shaper_graph_creator, self.timeout) + axes_shaper_calibration(gcmd, self._pconfig, st_process) def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None: ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') vibration_profile_creator = VibrationsGraphCreator(self._config) - st_thread = ShakeTuneThread(self._config, vibration_profile_creator, self.timeout) - create_vibrations_profile(gcmd, self._pconfig, st_thread) + st_process = ShakeTuneProcess(self._config, vibration_profile_creator, self.timeout) + create_vibrations_profile(gcmd, self._pconfig, st_process) diff --git a/shaketune/shaketune_thread.py b/shaketune/shaketune_process.py similarity index 54% rename from shaketune/shaketune_thread.py rename to shaketune/shaketune_process.py index 3b2de5d..faa4fa0 100644 --- a/shaketune/shaketune_thread.py +++ b/shaketune/shaketune_process.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 - import os +import multiprocessing import threading import traceback from typing import Optional @@ -10,51 +10,58 @@ from .helpers.console_output import ConsoleOutput from .shaketune_config import ShakeTuneConfig -class ShakeTuneThread(threading.Thread): +class ShakeTuneProcess: def __init__(self, config: ShakeTuneConfig, graph_creator, timeout: Optional[float] = None) -> None: - super(ShakeTuneThread, self).__init__() self._config = config self.graph_creator = graph_creator self._timeout = timeout - self._internal_thread = None - self._stop_event = threading.Event() + self._process = None def get_graph_creator(self): return self.graph_creator - def run(self) -> None: - # Start the target function in a new thread - self._internal_thread = threading.Thread(target=self._shaketune_thread, args=(self.graph_creator,)) - self._internal_thread.start() - - # If a timeout is specified, start a timer thread to monitor the timeout - if self._timeout is not None: - timer_thread = threading.Timer(self._timeout, self._handle_timeout) - timer_thread.start() - - def _handle_timeout(self) -> None: - if self._internal_thread.is_alive(): - self._stop_event.set() - ConsoleOutput.print('Timeout: Shake&Tune computation did not finish within the specified timeout!') + def start(self) -> None: + # Start the target function in a new process (a thread is known to cause issues with Klipper and CANbus due to the GIL) + self._process = multiprocessing.Process( + target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout) + ) + self._process.start() def wait_for_completion(self) -> None: - if self._internal_thread is not None: - self._internal_thread.join() + if self._process is not None: + self._process.join() - # This function run in a thread is used to do the CSV analysis and create the graphs - def _shaketune_thread(self, graph_creator) -> None: - # Trying to reduce the Shake&Tune post-processing thread priority to avoid slowing down the main Klipper process + # This function is a simple wrapper to start the Shake&Tune process. It's needed in order to get the timeout + # as a Timer in a thread INSIDE the Shake&Tune child process to not interfere with the main Klipper process + def _shaketune_process_wrapper(self, graph_creator, timeout) -> None: + if timeout is not None: + timer = threading.Timer(timeout, self._handle_timeout) + timer.start() + + try: + self._shaketune_process(graph_creator) + finally: + if timeout is not None: + timer.cancel() + + def _handle_timeout(self) -> None: + ConsoleOutput.print('Timeout: Shake&Tune computation did not finish within the specified timeout!') + os._exit(1) # Forcefully exit the process + + def _shaketune_process(self, graph_creator) -> None: + # Trying to reduce Shake&Tune process priority to avoid slowing down the main Klipper process # as this could lead to random "Timer too close" errors when already running CANbus, etc... try: - os.nice(20) + os.nice(15) except Exception: - ConsoleOutput.print('Warning: failed reducing Shake&Tune thread priority, continuing...') + ConsoleOutput.print('Warning: failed reducing Shake&Tune process priority, continuing...') # Ensure the output folders exist for folder in self._config.get_results_subfolders(): folder.mkdir(parents=True, exist_ok=True) + # Generate the graphs try: graph_creator.create_graph() except FileNotFoundError as e: