[PATCH 1/9] rteval: Fix timerlat error handling to prevent hangs
John Kacur <[email protected]> Tue, 5 May 2026 13:43:45 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Fixes multiple issues identified in RHEL-140898: 1. Limit SIGINT attempts to avoid infinite loop - Send maximum of 5 SIGINT signals with 2s intervals - Force SIGKILL if process doesn't respond after 10s - Check and log exit status 2. Handle partial output from rtla gracefully - Wrap histogram parsing in try/except/finally - Catch IndexError/ValueError when parsing bucket data - Log warnings instead of crashing on malformed output 3. Ensure _setFinished() is always called - Use finally block to signal completion even on errors - Prevents hang in WaitForCompletion when parsing fails 4. Fix bare except clause - Changed to catch ValueError specifically 5. Fix typo in docstring (objectd -> object) The root cause (rtla segfault) is being fixed upstream in RHEL-151475, but rteval must handle partial output gracefully since new rtla can still produce incomplete output if force-aborted during cleanup. Assisted-by: Claude Sonnet 4.5 <[email protected]> Signed-off-by: John Kacur <[email protected]> --- rteval/modules/measurement/timerlat.py | 318 ++++++++++++++----------- 1 file changed, 173 insertions(+), 145 deletions(-) diff --git a/rteval/modules/measurement/timerlat.py b/rteval/modules/measurement/timerlat.py index 4dfe18cc5492..2172f6ece520 100644 --- a/rteval/modules/measurement/timerlat.py +++ b/rteval/modules/measurement/timerlat.py @@ -2,7 +2,7 @@ # # Copyright 2024 John Kacur <[email protected]> # -""" timerlat.py - objectd to manage rtla timerlat """ +""" timerlat.py - object to manage rtla timerlat """ import os import subprocess import signal @@ -282,172 +282,200 @@ class Timerlat(rtevalModulePrototype): def _WorkloadCleanup(self): if not self.__started: return - while self.__timerlat_process.poll() is None: - self._log(Log.DEBUG, "Sending SIGINT") + + # Send SIGINT and wait for graceful exit + # Limit attempts to avoid infinite loop and double-SIGINT issues (RHEL-140898) + max_attempts = 5 + attempt = 0 + while self.__timerlat_process.poll() is None and attempt < max_attempts: + self._log(Log.DEBUG, f"Sending SIGINT (attempt {attempt + 1}/{max_attempts})") os.kill(self.__timerlat_process.pid, signal.SIGINT) time.sleep(2) + attempt += 1 + # Check if process exited + exit_code = self.__timerlat_process.returncode + if exit_code is None: + # Process still running after max attempts, force kill + self._log(Log.WARN, "timerlat did not respond to SIGINT, sending SIGKILL") + os.kill(self.__timerlat_process.pid, signal.SIGKILL) + self.__timerlat_process.wait() + exit_code = self.__timerlat_process.returncode + elif exit_code != 0: + self._log(Log.WARN, f"timerlat exited with non-zero status: {exit_code}") - # Parse histogram output - self.__timerlat_out.seek(0) - blocking_thread_detected = False - softirq_interference_detected = False - irq_interference_detected = False + # Parse histogram output - use try/finally to ensure _setFinished() is always called + # This prevents hangs if parsing fails due to partial output (RHEL-140898) + try: + self.__timerlat_out.seek(0) - for line in self.__timerlat_out: - line = bytes.decode(line) + blocking_thread_detected = False + softirq_interference_detected = False + irq_interference_detected = False - # Skip any blank lines - if not line: - continue + for line in self.__timerlat_out: + line = bytes.decode(line) - # Parsing if stoptrace has been invoked - if self.__stoptrace: - self.__posttrace += line - line = line.strip() - fields = line.split() + # Skip any blank lines if not line: continue - if line.startswith("##") and fields[1] == "CPU": - self.stcpu = int(fields[2]) - self._log(Log.DEBUG, f"self.stcpu = {self.stcpu}") - self.__stdata[self.stcpu] = {} + + # Parsing if stoptrace has been invoked + if self.__stoptrace: + self.__posttrace += line + line = line.strip() + fields = line.split() + if not line: + continue + if line.startswith("##") and fields[1] == "CPU": + self.stcpu = int(fields[2]) + self._log(Log.DEBUG, f"self.stcpu = {self.stcpu}") + self.__stdata[self.stcpu] = {} + continue + if self.stcpu == -1: + self._log(Log.WARN, "Stop trace has been invoked, but a stop cpu has not been identified.") + continue + if line.startswith('------------------'): + blocking_thread_detected = False + softirq_interference_detected = False + irq_interference_detected = False + continue + + # work around rtla not printing ':' after all names + if line.startswith('Softirq interference'): + name = 'Softirq_interference' + elif line.startswith('IRQ interference'): + name = 'IRQ_interference' + else: + name = ''.join(line.split(':')[0]).replace(' ', '_') + self._log(Log.DEBUG, f"name={name}") + + if name in ['Thread_latency']: + latency = fields[-3] + percent = fields[-1].strip('()%') + self._log(Log.DEBUG, f'{name} = ({latency}, {percent})') + self.__stdata[self.stcpu][name] = (latency, percent) + continue + if name in ['Timerlat_IRQ_duration', 'IRQ_handler_delay', 'Blocking_thread', 'IRQ_interference', 'Softirq_interference']: + latency = fields[-4] + percent = fields[-2].strip('(') + if name == 'IRQ_handler_delay' and fields[3] == '(exit': + name = 'IRQ_handler_delay_exit_from_idle' + self._log(Log.DEBUG, f'{name} = ({latency}, {percent})') + self.__stdata[self.stcpu][name] = (latency, percent) + detected = {'Blocking_thread' : (True, False, False), + 'IRQ_interference' : (False, True, False), + 'Softirq_interference' : (False, False, True) } + if name in ('Blocking_thread', 'IRQ_interference', 'Softirq_interference'): + blocking_thread_detected, irq_interference_detected, softirq_interference_detected = detected.get(name) + continue + if name in ["IRQ_latency", "Previous_IRQ_interference"]: + latency = fields[-2] + self._log(Log.DEBUG, f'{name} = {fields[-2]}') + self.__stdata[self.stcpu][name] = fields[-2] + continue + if blocking_thread_detected or softirq_interference_detected or irq_interference_detected: + if blocking_thread_detected: + field_name = "blocking_thread" + elif softirq_interference_detected: + field_name = "softirq_interference" + elif irq_interference_detected: + field_name = "irq_interference" + thread = " ".join(fields[0:-2]) + latency = fields[-2] + self._log(Log.DEBUG, f"{field_name} += [({thread}, {latency})]") + self.__stdata[self.stcpu].setdefault(field_name, []) + self.__stdata[self.stcpu][field_name] += [(thread, latency)] + continue + if name == "Max_timerlat_IRQ_latency_from_idle": + latency = fields[-5] + max_timerlat_cpu = int(fields[-1]) + self._log(Log.DEBUG, f'self.__stdata[{max_timerlat_cpu}][{name}] = {latency}') + self.__stdata.setdefault(max_timerlat_cpu, {}) + self.__stdata[max_timerlat_cpu][name] = latency + else: + self._log(Log.DEBUG, f'line = {line}') + continue + + if line.startswith('#'): + if line.startswith('# Duration:'): + duration = line.split()[2] + duration += line.split()[3] + self.__timerlatdata['system'].duration = duration continue - if self.stcpu == -1: - self._log(Log.WARN, "Stop trace has been invoked, but a stop cpu has not been identified.") + elif line.startswith('Index'): + #print(line) continue - if line.startswith('------------------'): - blocking_thread_detected = False - softirq_interference_detected = False - irq_interference_detected = False + elif line.startswith('over:'): + #print(line) continue - - # work around rtla not printing ':' after all names - if line.startswith('Softirq interference'): - name = 'Softirq_interference' - elif line.startswith('IRQ interference'): - name = 'IRQ_interference' - else: - name = ''.join(line.split(':')[0]).replace(' ', '_') - self._log(Log.DEBUG, f"name={name}") - - if name in ['Thread_latency']: - latency = fields[-3] - percent = fields[-1].strip('()%') - self._log(Log.DEBUG, f'{name} = ({latency}, {percent})') - self.__stdata[self.stcpu][name] = (latency, percent) + elif line.startswith('count:'): + #print(line) + continue + elif line.startswith('min:'): + #print(line) continue - if name in ['Timerlat_IRQ_duration', 'IRQ_handler_delay', 'Blocking_thread', 'IRQ_interference', 'Softirq_interference']: - latency = fields[-4] - percent = fields[-2].strip('(') - if name == 'IRQ_handler_delay' and fields[3] == '(exit': - name = 'IRQ_handler_delay_exit_from_idle' - self._log(Log.DEBUG, f'{name} = ({latency}, {percent})') - self.__stdata[self.stcpu][name] = (latency, percent) - detected = {'Blocking_thread' : (True, False, False), - 'IRQ_interference' : (False, True, False), - 'Softirq_interference' : (False, False, True) } - if name in ('Blocking_thread', 'IRQ_interference', 'Softirq_interference'): - blocking_thread_detected, irq_interference_detected, softirq_interference_detected = detected.get(name) + elif line.startswith('avg:'): + #print(line) continue - if name in ["IRQ_latency", "Previous_IRQ_interference"]: - latency = fields[-2] - self._log(Log.DEBUG, f'{name} = {fields[-2]}') - self.__stdata[self.stcpu][name] = fields[-2] + elif line.startswith('max:'): + #print(line) continue - if blocking_thread_detected or softirq_interference_detected or irq_interference_detected: - if blocking_thread_detected: - field_name = "blocking_thread" - elif softirq_interference_detected: - field_name = "softirq_interference" - elif irq_interference_detected: - field_name = "irq_interference" - thread = " ".join(fields[0:-2]) - latency = fields[-2] - self._log(Log.DEBUG, f"{field_name} += [({thread}, {latency})]") - self.__stdata[self.stcpu].setdefault(field_name, []) - self.__stdata[self.stcpu][field_name] += [(thread, latency)] + elif line.startswith('rtla timerlat hit stop tracing'): + self.__stoptrace = True + self.__posttrace += line + continue + elif line.startswith('ALL:'): + # We should only see 'ALL:' without timerlat --no-summary + # print(line) continue - if name == "Max_timerlat_IRQ_latency_from_idle": - latency = fields[-5] - max_timerlat_cpu = int(fields[-1]) - self._log(Log.DEBUG, f'self.__stdata[{max_timerlat_cpu}][{name}] = {latency}') - self.__stdata.setdefault(max_timerlat_cpu, {}) - self.__stdata[max_timerlat_cpu][name] = latency else: - self._log(Log.DEBUG, f'line = {line}') - continue + #print(line) + pass - if line.startswith('#'): - if line.startswith('# Duration:'): - duration = line.split()[2] - duration += line.split()[3] - self.__timerlatdata['system'].duration = duration - continue - elif line.startswith('Index'): - #print(line) - continue - elif line.startswith('over:'): - #print(line) - continue - elif line.startswith('count:'): - #print(line) - continue - elif line.startswith('min:'): - #print(line) - continue - elif line.startswith('avg:'): - #print(line) - continue - elif line.startswith('max:'): - #print(line) - continue - elif line.startswith('rtla timerlat hit stop tracing'): - self.__stoptrace = True - self.__posttrace += line - continue - elif line.startswith('ALL:'): - # We should only see 'ALL:' without timerlat --no-summary - # print(line) - continue - else: - #print(line) - pass + vals = line.split() + if not vals: + # If we don't have any values, don't try parsing + continue + try: + # The index corresponds to the bucket number + index = int(vals[0]) + except ValueError: + self._log(Log.DEBUG, f'timerlat: unexpected output: {line}') + continue - vals = line.split() - if not vals: - # If we don't have any values, don't try parsing - continue - try: - # The index corresponds to the bucket number - index = int(vals[0]) - except: - self._log(Log.DEBUG, f'timerlat: unexpected output: {line}') - continue + for i, core in enumerate(self.__cpus): + # There might not be a count on every cpu if tracing invoked + try: + if i*3 + 1 >= len(vals): + self.__timerlatdata[core].bucket(index, 0, 0, 0) + self.__timerlatdata['system'].bucket(index, 0, 0, 0) + else: + self.__timerlatdata[core].bucket(index, int(vals[i*3+1]), + int(vals[i*3+2]), + int(vals[i*3+3])) + self.__timerlatdata['system'].bucket(index, int(vals[i*3+1]), + int(vals[i*3+2]), + int(vals[i*3+3])) + except (IndexError, ValueError) as e: + # Handle partial output from rtla (can happen on SIGINT during cleanup) + self._log(Log.WARN, f"Error parsing timerlat bucket data for core {core}: {e}") + continue - for i, core in enumerate(self.__cpus): - # There might not be a count on every cpu if tracing invoked - if i*3 + 1 >= len(vals): - self.__timerlatdata[core].bucket(index, 0, 0, 0) - self.__timerlatdata['system'].bucket(index, 0, 0, 0) - else: - self.__timerlatdata[core].bucket(index, int(vals[i*3+1]), - int(vals[i*3+2]), - int(vals[i*3+3])) - self.__timerlatdata['system'].bucket(index, int(vals[i*3+1]), - int(vals[i*3+2]), - int(vals[i*3+3])) - - # Generate statistics for each RunData object - for n in list(self.__timerlatdata.keys()): - self.__timerlatdata[n].reduce() - - self._setFinished() - self.__started = False + # Generate statistics for each RunData object + for n in list(self.__timerlatdata.keys()): + self.__timerlatdata[n].reduce() + + except Exception as e: + self._log(Log.ERR, f"Error parsing timerlat output: {e}") + finally: + # Always signal completion to avoid hangs + self._setFinished() + self.__started = False - self.__timerlat_err.close() - self.__timerlat_out.close() + self.__timerlat_err.close() + self.__timerlat_out.close() def MakeReport(self): rep_n = libxml2.newNode('timerlat') -- 2.54.0