"""Reusable background workers for dense plots and viewport measurements.""" from collections import deque # Кольцевые очереди живого графика требуют единого снимка между проходами амплитудной статистики и частоты. try: from PySide6.QtCore import QObject, QRunnable, Signal except ImportError: from PySide2.QtCore import QObject, QRunnable, Signal from set_devices.plot_decimation import pixel_envelope, render_envelope, viewport_samples # Рабочее задание поддерживает точный прежний формат и компактную экранную огибающую для аналогового графика. from set_devices.oscilloscope import measurements from set_devices.background_work import WorkBudget # Даже фоновый Python-расчёт должен регулярно отдавать GIL многочисленным обработчикам интерфейса Qt. class EnvelopeSignals(QObject): ready = Signal(str, object, object) class EnvelopeJob(QRunnable): def __init__(self, key, geometry, timestamps, values, left, right, width, signals, compact=False): # Необязательный режим оставляет прежний контракт внешних пользователей, а аналоговый график включает два экстремума на столбец. super().__init__() self.args = (timestamps, values, left, right, width) self.key, self.geometry, self.signals = key, geometry, signals self.select = render_envelope if compact else pixel_envelope # Выбор алгоритма выполняется один раз при создании задания, не внутри многомиллионного обхода. def run(self): try: points = tuple(self.select(*self.args)) # Фоновый результат сохраняет точные пики и разрывы в согласованном с рисователем экранном бюджете. except Exception: points = None try: self.signals.ready.emit(self.key, self.geometry, points) except RuntimeError: pass # The plot may have been closed while the worker was running. class NavigationSignals(QObject): ready = Signal(int, object, object) class _NavigationSamples: # Повторяемая выборка окна позволяет измерять исходные ряды без копии миллионов пар координат. def __init__(self, times, values, bounds, check_cancelled): # Храним источники задания; вызывающий код создаёт снимок изменяемых очередей перед численными проходами. self.times, self.values, self.bounds, self.check_cancelled = times, values, bounds, check_cancelled # Ссылки сохраняют исходную точность и порядок без экранного сокращения. self.count = 0 # Число отсчётов определяется при первом полном проходе и включает нечисловые амплитуды, как прежний список. def __iter__(self): # Статистики и частота независимо обходят то же окно, не удерживая промежуточные пары в памяти. count = 0 # Каждый проход считает только точки внутри включительных временных границ. for index, (x, y) in enumerate(viewport_samples(self.times, self.values, self.bounds.left, self.bounds.right)): # Индексируемый источник ищет окно двоичным поиском; потоковый сохраняет прежний порядок. if index % 1024 == 0: # Проверяем отмену через 1024 просмотренных отсчёта, даже когда неупорядоченный ряд лежит вне окна. self.check_cancelled() # Не продолжаем чтение старого диапазона после нового жеста навигации. if self.bounds.left <= x <= self.bounds.right: # Включаем обе границы и все дубликаты временных меток, как исходная реализация. count += 1 # Счётчик соответствует прежнему len(points), независимо от конечности амплитуды. yield x, y # Пара существует только на время текущего численного прохода и не копируется в большой список. self.count = count # Публикуем количество только после полного прохода, чтобы отменённая выборка не выдавала частичный результат. class NavigationJob(QRunnable): def __init__(self, revision, series, bounds, emitter, cancelled=None): # Необязательное событие отмены сохраняет совместимость общих графиков и останавливает устаревшие измерения. super().__init__() self.revision, self.series, self.bounds, self.emitter = revision, series, bounds, emitter self.cancelled = cancelled # Событие принадлежит одному заданию и не сбрасывается при запуске расчёта нового окна. self.work_budget = WorkBudget(cancelled) # Короткие добровольные паузы предотвращают многосекундное ожидание GIL главным потоком. def _check_cancelled(self): # Проверяем отмену на границах этапов и небольших блоков исходных отсчётов. self.work_budget() # Одновременно прекращаем устаревшие измерения и регулярно освобождаем GIL для рисования и событий мыши. def run(self): stats = {} try: for key, times, values in self.series: self._check_cancelled() # Не начинаем обход следующего канала после возобновления перемещения или изменения масштаба. samples = _NavigationSamples(times, values, self.bounds, self._check_cancelled) # Повторяемый вид файлового окна устраняет копирование миллионов пар и сохраняет отмену чтения источника. ordered = getattr(times, "ordered", isinstance(times, (list, tuple, range))) # Применяем тот же контракт упорядоченности, что и viewport_samples, включая смещённые файловые координаты. points = tuple(samples) if isinstance(times, deque) or isinstance(values, deque) or not ordered else samples # Только живые очереди и неупорядоченный путь получают единый снимок для согласованных амплитуд и частоты. result = measurements(points, self._check_cancelled) # Передаём отмену внутрь всех численных этапов, чтобы новый жест не ждал окончания полного среднего или СКЗ. stats[key] = result, samples.count # Количество относится к завершённому проходу того же окна либо к первоначальному снимку живого ряда. self._check_cancelled() # При отмене во время вычислений не продолжаем остальные каналы и не публикуем устаревшие значения. self.emitter.ready.emit(self.revision, stats, self.bounds) except Exception: try: self.emitter.ready.emit(self.revision, {}, self.bounds) except RuntimeError: pass