Files
RoboGauge/robogauge/utils/progress_monitor.py
2025-12-29 21:29:03 +08:00

174 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
'''
@File : progress_monitor.py
@Time : 2025/12/27 00:10:07
@Author : wty-yy, Gemini3 Pro
@Version : 1.0
@Blog : https://wty-yy.github.io/
@Desc : Centralized progress monitoring with Dynamic Slot Management
'''
from tqdm import tqdm
import multiprocessing
from typing import Dict
from threading import Thread
from dataclasses import dataclass
import heapq
class ProgressTypes:
INIT = 'init' # Init progress (set total, desc)
UPDATE = 'update' # Update progress value (set value)
DESC = 'desc' # Update description text only (set desc)
RESET = 'reset' # Reset progress bar (set total, desc)
FINISH = 'finish' # Mark completion (set desc)
ERROR = 'error' # Mark error (set desc)
@dataclass
class ProgressData:
progress_queue: multiprocessing.Queue
task_id: int = 0
msg_prefix: str = ''
def report_progress(progress_data: ProgressData, msg_type, value=None, desc=None, total=None):
"""
Assistant function to report progress to the main process.
Args:
queue: multiprocessing.Queue
task_id: int, line number of the task
msg_type: ProgressTypes
value: int, value to update (for UPDATE type)
desc: str, description text (for DESC, INIT, FINISH types)
total: int, total value (for INIT, RESET types)
"""
if progress_data is None:
return
queue = progress_data.progress_queue
task_id = progress_data.task_id
msg_prefix = progress_data.msg_prefix
if desc is not None:
desc = msg_prefix + desc
queue.put((task_id, msg_type, {'value': value, 'desc': desc, 'total': total}))
class ProgressMonitor:
def __init__(self, total_rows):
self.total_rows = total_rows
self.active_bars: Dict[int, Dict] = {}
self.free_slots = []
self.max_slot_used = 0
def _get_free_slot(self):
""" Get a free screen display position """
if self.free_slots:
return heapq.heappop(self.free_slots)
else:
self.max_slot_used += 1
return self.max_slot_used
def _release_slot(self, slot):
""" Release a screen display position """
heapq.heappush(self.free_slots, slot)
def listener_loop(self, queue):
"""
Listener thread in the main process
"""
# Create the main progress bar (Position 0)
main_bar = tqdm(
total=self.total_rows,
position=0,
desc="🚀 Total Progress",
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [Done: {n_fmt}] [{elapsed}<{remaining}]",
leave=True,
smoothing=0.1 # Optional: set smoothing factor (0-1) to prevent drastic ETA fluctuations due to slow tasks
)
# Number of tasks still pending
pending_tasks = self.total_rows
while pending_tasks > 0:
record = queue.get()
if record is None: break
task_id, msg_type, data = record
# --- Dynamically create/get sub progress bars ---
if task_id not in self.active_bars:
# Only create bar when message received to avoid Pending screen flicker
slot = self._get_free_slot()
# leave=False is key: after task completion, clear the line for new task reuse
bar = tqdm(
total=100,
position=slot,
desc=f"Task {task_id} Starting...",
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
leave=False
)
self.active_bars[task_id] = {'bar': bar, 'slot': slot}
bar_info = self.active_bars[task_id]
bar = bar_info['bar']
# --- Handle messages ---
try:
if msg_type == ProgressTypes.INIT:
total = data.get('total', 100)
desc = data.get('desc', '')
bar.reset(total=total)
bar.set_description(desc)
bar.refresh()
elif msg_type == ProgressTypes.UPDATE:
val = data.get('value', 1)
bar.update(val)
elif msg_type == ProgressTypes.DESC:
desc = data.get('desc')
if desc: bar.set_description(desc)
elif msg_type == ProgressTypes.RESET:
total = data.get('total', 100)
desc = data.get('desc', '')
bar.reset(total=total)
bar.set_description(desc)
bar.refresh()
elif msg_type == ProgressTypes.FINISH or msg_type == ProgressTypes.ERROR:
# Update sub progress bar status
desc = data.get('desc')
if desc: bar.set_description(desc)
bar.refresh()
# Close sub progress bar and release slot
bar.close() # 因为 leave=False这行会从屏幕消失
slot = bar_info['slot']
self._release_slot(slot)
del self.active_bars[task_id]
# Update the main progress bar
main_bar.update(1)
main_bar.refresh()
pending_tasks -= 1
except Exception as e:
pass # Prevent printing errors from disrupting layout
# Close all remaining bars (theoretically should be empty except main_bar)
main_bar.close()
for info in self.active_bars.values():
info['bar'].close()
def start_progress_monitor_thread(total_rows):
"""
Create and start a ProgressMonitor thread. Queue is returned for reporting progress.
Args:
total_rows: int, number of tasks to monitor
Returns:
queue: multiprocessing.Queue
monitor_thread: threading.Thread
"""
queue = multiprocessing.Manager().Queue()
monitor = ProgressMonitor(total_rows)
monitor_thread = Thread(target=monitor.listener_loop, args=(queue,))
monitor_thread.start()
return queue, monitor_thread