Feature: Detaillierte Worker-Pool Performance-Metriken mit psutil

Neue Metrik-Erfassung für Saxon- und FOP-Worker-Pools:
- Kompilierungszeit der Java-Worker-Klassen
- Worker-Startzeiten (Summe + Durchschnitt pro Worker)
- RAM-Verbrauch vor/nach Transformation (Summe + Durchschnitt)
- Automatische Berechnung der RAM-Zunahme in MB und Prozent

Technische Details:
- Neue WorkerPoolMetrics-Datenklasse in worker_metrics.py
- RAM-Messung via psutil (v7.2.1, neu hinzugefügt)
- Metriken für beide Saxon-Varianten (JAXP + s9api)
- WorkerPoolMetricsDialog mit Tab-basierter UI
- Menüeintrag "Projekt → Worker-Pool-Metriken"

Metriken werden automatisch erfasst:
- Bei Worker-Pool-Initialisierung (Kompilierung + Start)
- Vor erster Transformation (RAM-Baseline)
- Nach allen Transformationen (RAM-Endwert)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-06 20:58:37 +01:00
parent cfbdc476fa
commit d3dc07cbf3
8 changed files with 624 additions and 12 deletions
+1
View File
@@ -11,6 +11,7 @@ dependencies = [
"pyside6>=6.9.1",
"polars[connectorx,pyarrow]>=1.31.0",
"pydantic-yaml>=1.5.1",
"psutil>=6.1.1",
]
[tool.ruff]
+78 -4
View File
@@ -8,11 +8,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen
import logging
import subprocess
import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
from worker_metrics import WorkerPoolMetrics
logger = logging.getLogger(__name__)
# Java-Worker-Code (wird zur Laufzeit kompiliert)
@@ -206,6 +210,9 @@ class FopWorkerPool:
# Classpath für FOP
self.fop_classpath: Optional[str] = None
# Performance-Metriken
self.metrics = WorkerPoolMetrics()
# Initialisierung
self._build_fop_classpath()
self._compile_worker_class()
@@ -236,6 +243,7 @@ class FopWorkerPool:
def _compile_worker_class(self):
"""Kompiliert die FopWorker-Java-Klasse."""
start_time = time.time()
try:
# Erstelle temporäres Verzeichnis
self.temp_dir = Path(tempfile.mkdtemp(prefix="fop_worker_"))
@@ -261,7 +269,12 @@ class FopWorkerPool:
self.worker_class_path = self.temp_dir
logger.info(f"FopWorker erfolgreich kompiliert: {self.temp_dir}")
# Speichere Kompilierungszeit
self.metrics.compilation_time_seconds = time.time() - start_time
logger.info(
f"FopWorker erfolgreich kompiliert: {self.temp_dir} " f"({self.metrics.compilation_time_seconds:.3f}s)"
)
except Exception as e:
logger.error(f"Fehler beim Kompilieren von FopWorker: {e}")
@@ -281,6 +294,7 @@ class FopWorkerPool:
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
for i in range(self.num_workers):
worker_start_time = time.time()
try:
# Starte JVM-Prozess mit FopWorker
# Übergebe fop.xconf als Argument falls vorhanden
@@ -308,8 +322,6 @@ class FopWorkerPool:
logger.debug(f"FOP Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
# Warte kurz damit Worker initialisieren kann
import time
time.sleep(0.2) # FOP braucht etwas länger zum Initialisieren
# Prüfe ob Worker noch läuft
@@ -322,11 +334,22 @@ class FopWorkerPool:
f"FOP Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
)
# Speichere Worker-Startzeit
worker_elapsed = time.time() - worker_start_time
self.metrics.worker_start_times.append(worker_elapsed)
except Exception as e:
logger.error(f"Fehler beim Starten von FOP Worker {i}: {e}")
raise
logger.info(f"{len(self.workers)} FOP-Worker erfolgreich gestartet")
# Berechne Aggregat-Werte für Worker-Startzeiten
self.metrics.calculate_aggregates()
logger.info(
f"{len(self.workers)} FOP-Worker erfolgreich gestartet "
f"(Summe: {self.metrics.total_worker_start_time_seconds:.3f}s, "
f"Durchschnitt: {self.metrics.average_worker_start_time_seconds:.3f}s)"
)
def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]:
"""
@@ -410,6 +433,57 @@ class FopWorkerPool:
# Gebe Worker-Lock frei
self.worker_locks[worker_idx].release()
def measure_ram_usage(self) -> tuple[float, float, list[float]]:
"""
Misst den aktuellen RAM-Verbrauch aller Worker-Prozesse.
Returns:
tuple: (total_mb, average_mb, per_worker_mb_list)
"""
ram_per_worker = []
for i, worker in enumerate(self.workers):
try:
if worker.poll() is None: # Worker läuft noch
process = psutil.Process(worker.pid)
# Hole Speicherinfo (RSS = Resident Set Size in Bytes)
mem_info = process.memory_info()
ram_mb = mem_info.rss / (1024 * 1024) # Konvertiere zu MB
ram_per_worker.append(ram_mb)
else:
logger.warning(f"Worker {i} ist nicht mehr aktiv (kann RAM nicht messen)")
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
logger.warning(f"Konnte RAM für Worker {i} nicht messen: {e}")
total_ram = sum(ram_per_worker)
average_ram = total_ram / len(ram_per_worker) if ram_per_worker else 0.0
return total_ram, average_ram, ram_per_worker
def capture_ram_before_transform(self):
"""Erfasst RAM-Verbrauch vor der ersten Transformation."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_before_transform_mb_per_worker = per_worker
self.metrics.total_ram_before_mb = total
self.metrics.average_ram_before_mb = average
logger.info(
f"RAM vor Transformation: {self.metrics.total_ram_before_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_before_mb:.1f} MB/Worker)"
)
def capture_ram_after_transform(self):
"""Erfasst RAM-Verbrauch nach allen Transformationen."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_after_transform_mb_per_worker = per_worker
self.metrics.total_ram_after_mb = total
self.metrics.average_ram_after_mb = average
logger.info(
f"RAM nach Transformation: {self.metrics.total_ram_after_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_after_mb:.1f} MB/Worker)"
)
def shutdown(self):
"""Beendet alle Worker-Prozesse sauber."""
logger.info("Beende FOP-Worker-Pool...")
+78 -4
View File
@@ -8,11 +8,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheina
import logging
import subprocess
import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
from worker_metrics import WorkerPoolMetrics
logger = logging.getLogger(__name__)
# Java-Worker-Code (wird zur Laufzeit kompiliert)
@@ -197,6 +201,9 @@ class SaxonWorkerPool:
self.worker_class_path: Optional[Path] = None
self.worker_log_dir: Optional[Path] = None
# Performance-Metriken
self.metrics = WorkerPoolMetrics()
# Initialisierung
self._compile_worker_class()
self._start_workers()
@@ -205,6 +212,7 @@ class SaxonWorkerPool:
def _compile_worker_class(self):
"""Kompiliert die SaxonWorker-Java-Klasse."""
start_time = time.time()
try:
# Erstelle temporäres Verzeichnis
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
@@ -242,7 +250,12 @@ class SaxonWorkerPool:
self.worker_class_path = self.temp_dir
logger.info(f"SaxonWorker erfolgreich kompiliert: {self.temp_dir}")
# Speichere Kompilierungszeit
self.metrics.compilation_time_seconds = time.time() - start_time
logger.info(
f"SaxonWorker erfolgreich kompiliert: {self.temp_dir} " f"({self.metrics.compilation_time_seconds:.3f}s)"
)
except Exception as e:
logger.error(f"Fehler beim Kompilieren von SaxonWorker: {e}")
@@ -266,6 +279,7 @@ class SaxonWorkerPool:
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
for i in range(self.num_workers):
worker_start_time = time.time()
try:
# Starte JVM-Prozess mit SaxonWorker
cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
@@ -289,8 +303,6 @@ class SaxonWorkerPool:
logger.debug(f"Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
# Warte kurz damit Worker initialisieren kann
import time
time.sleep(0.1)
# Prüfe ob Worker noch läuft
@@ -303,11 +315,22 @@ class SaxonWorkerPool:
f"Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
)
# Speichere Worker-Startzeit
worker_elapsed = time.time() - worker_start_time
self.metrics.worker_start_times.append(worker_elapsed)
except Exception as e:
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
raise
logger.info(f"{len(self.workers)} Saxon-Worker erfolgreich gestartet")
# Berechne Aggregat-Werte für Worker-Startzeiten
self.metrics.calculate_aggregates()
logger.info(
f"{len(self.workers)} Saxon-Worker erfolgreich gestartet "
f"(Summe: {self.metrics.total_worker_start_time_seconds:.3f}s, "
f"Durchschnitt: {self.metrics.average_worker_start_time_seconds:.3f}s)"
)
def transform(
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
@@ -398,6 +421,57 @@ class SaxonWorkerPool:
# Gebe Worker-Lock frei
self.worker_locks[worker_idx].release()
def measure_ram_usage(self) -> tuple[float, float, list[float]]:
"""
Misst den aktuellen RAM-Verbrauch aller Worker-Prozesse.
Returns:
tuple: (total_mb, average_mb, per_worker_mb_list)
"""
ram_per_worker = []
for i, worker in enumerate(self.workers):
try:
if worker.poll() is None: # Worker läuft noch
process = psutil.Process(worker.pid)
# Hole Speicherinfo (RSS = Resident Set Size in Bytes)
mem_info = process.memory_info()
ram_mb = mem_info.rss / (1024 * 1024) # Konvertiere zu MB
ram_per_worker.append(ram_mb)
else:
logger.warning(f"Worker {i} ist nicht mehr aktiv (kann RAM nicht messen)")
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
logger.warning(f"Konnte RAM für Worker {i} nicht messen: {e}")
total_ram = sum(ram_per_worker)
average_ram = total_ram / len(ram_per_worker) if ram_per_worker else 0.0
return total_ram, average_ram, ram_per_worker
def capture_ram_before_transform(self):
"""Erfasst RAM-Verbrauch vor der ersten Transformation."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_before_transform_mb_per_worker = per_worker
self.metrics.total_ram_before_mb = total
self.metrics.average_ram_before_mb = average
logger.info(
f"RAM vor Transformation: {self.metrics.total_ram_before_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_before_mb:.1f} MB/Worker)"
)
def capture_ram_after_transform(self):
"""Erfasst RAM-Verbrauch nach allen Transformationen."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_after_transform_mb_per_worker = per_worker
self.metrics.total_ram_after_mb = total
self.metrics.average_ram_after_mb = average
logger.info(
f"RAM nach Transformation: {self.metrics.total_ram_after_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_after_mb:.1f} MB/Worker)"
)
def shutdown(self):
"""Beendet alle Worker-Prozesse sauber."""
logger.info("Beende Saxon-Worker-Pool...")
+79 -4
View File
@@ -9,11 +9,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheina
import logging
import subprocess
import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
from worker_metrics import WorkerPoolMetrics
logger = logging.getLogger(__name__)
# Java-Worker-Code für s9api (wird zur Laufzeit kompiliert)
@@ -179,6 +183,9 @@ class SaxonWorkerPoolS9Api:
self.worker_class_path: Optional[Path] = None
self.worker_log_dir: Optional[Path] = None
# Performance-Metriken
self.metrics = WorkerPoolMetrics()
# Initialisierung
self._compile_worker_class()
self._start_workers()
@@ -187,6 +194,7 @@ class SaxonWorkerPoolS9Api:
def _compile_worker_class(self):
"""Kompiliert die SaxonS9ApiWorker-Java-Klasse."""
start_time = time.time()
try:
# Erstelle temporäres Verzeichnis
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_s9api_worker_"))
@@ -224,7 +232,13 @@ class SaxonWorkerPoolS9Api:
self.worker_class_path = self.temp_dir
logger.info(f"SaxonS9ApiWorker erfolgreich kompiliert: {self.temp_dir}")
# Speichere Kompilierungszeit
self.metrics.compilation_time_seconds = time.time() - start_time
logger.info(
f"SaxonS9ApiWorker erfolgreich kompiliert: {self.temp_dir} "
f"({self.metrics.compilation_time_seconds:.3f}s)"
)
except Exception as e:
logger.error(f"Fehler beim Kompilieren von SaxonS9ApiWorker: {e}")
@@ -267,6 +281,7 @@ class SaxonWorkerPoolS9Api:
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
for i in range(self.num_workers):
worker_start_time = time.time()
try:
# Starte JVM-Prozess mit SaxonS9ApiWorker
cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonS9ApiWorker"]
@@ -290,8 +305,6 @@ class SaxonWorkerPoolS9Api:
logger.debug(f"S9Api Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
# Warte kurz damit Worker initialisieren kann
import time
time.sleep(0.1)
# Prüfe ob Worker noch läuft
@@ -304,11 +317,22 @@ class SaxonWorkerPoolS9Api:
f"S9Api Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
)
# Speichere Worker-Startzeit
worker_elapsed = time.time() - worker_start_time
self.metrics.worker_start_times.append(worker_elapsed)
except Exception as e:
logger.error(f"Fehler beim Starten von S9Api Worker {i}: {e}")
raise
logger.info(f"{len(self.workers)} Saxon-S9Api-Worker erfolgreich gestartet")
# Berechne Aggregat-Werte für Worker-Startzeiten
self.metrics.calculate_aggregates()
logger.info(
f"{len(self.workers)} Saxon-S9Api-Worker erfolgreich gestartet "
f"(Summe: {self.metrics.total_worker_start_time_seconds:.3f}s, "
f"Durchschnitt: {self.metrics.average_worker_start_time_seconds:.3f}s)"
)
def transform(
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
@@ -399,6 +423,57 @@ class SaxonWorkerPoolS9Api:
# Gebe Worker-Lock frei
self.worker_locks[worker_idx].release()
def measure_ram_usage(self) -> tuple[float, float, list[float]]:
"""
Misst den aktuellen RAM-Verbrauch aller Worker-Prozesse.
Returns:
tuple: (total_mb, average_mb, per_worker_mb_list)
"""
ram_per_worker = []
for i, worker in enumerate(self.workers):
try:
if worker.poll() is None: # Worker läuft noch
process = psutil.Process(worker.pid)
# Hole Speicherinfo (RSS = Resident Set Size in Bytes)
mem_info = process.memory_info()
ram_mb = mem_info.rss / (1024 * 1024) # Konvertiere zu MB
ram_per_worker.append(ram_mb)
else:
logger.warning(f"Worker {i} ist nicht mehr aktiv (kann RAM nicht messen)")
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
logger.warning(f"Konnte RAM für Worker {i} nicht messen: {e}")
total_ram = sum(ram_per_worker)
average_ram = total_ram / len(ram_per_worker) if ram_per_worker else 0.0
return total_ram, average_ram, ram_per_worker
def capture_ram_before_transform(self):
"""Erfasst RAM-Verbrauch vor der ersten Transformation."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_before_transform_mb_per_worker = per_worker
self.metrics.total_ram_before_mb = total
self.metrics.average_ram_before_mb = average
logger.info(
f"RAM vor Transformation: {self.metrics.total_ram_before_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_before_mb:.1f} MB/Worker)"
)
def capture_ram_after_transform(self):
"""Erfasst RAM-Verbrauch nach allen Transformationen."""
total, average, per_worker = self.measure_ram_usage()
self.metrics.ram_after_transform_mb_per_worker = per_worker
self.metrics.total_ram_after_mb = total
self.metrics.average_ram_after_mb = average
logger.info(
f"RAM nach Transformation: {self.metrics.total_ram_after_mb:.1f} MB "
f"(Durchschnitt: {self.metrics.average_ram_after_mb:.1f} MB/Worker)"
)
def shutdown(self):
"""Beendet alle Worker-Prozesse sauber."""
logger.info("Beende Saxon-S9Api-Worker-Pool...")
+46
View File
@@ -1078,6 +1078,25 @@ class MainWindow(QMainWindow):
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog)
# Worker-Pool-Metriken Menüeintrag (programmatisch hinzufügen)
from PySide6.QtGui import QAction
self.action_worker_metrics = QAction("Worker-Pool-Metriken", self)
self.action_worker_metrics.triggered.connect(self._show_worker_pool_metrics)
# Füge die Aktion zum Projekt-Menü hinzu (nach Einstellungen, vor Beenden)
actions = self.ui.menuProjekt.actions()
# Finde die Position nach actionEinstellungen
insert_index = len(actions) # Fallback: Am Ende
for i, action in enumerate(actions):
if action == self.ui.actionEinstellungen:
insert_index = i + 1
break
before_action = actions[insert_index] if insert_index < len(actions) else None
if before_action:
self.ui.menuProjekt.insertAction(before_action, self.action_worker_metrics)
else:
self.ui.menuProjekt.addAction(self.action_worker_metrics)
# Button "lade aus FN2" verbinden
self.ui.pB_lade_aus_fn2.clicked.connect(self.on_load_from_fn2_clicked)
@@ -1525,6 +1544,17 @@ class MainWindow(QMainWindow):
except Exception as e:
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
def _show_worker_pool_metrics(self):
"""Zeigt den Worker-Pool-Metriken-Dialog an."""
try:
from ui.WorkerPoolMetricsDialog import WorkerPoolMetricsDialog
dialog = WorkerPoolMetricsDialog(self)
dialog.exec()
except Exception as e:
logger.error(f"Fehler beim Öffnen des Metriken-Dialogs: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Öffnen des Metriken-Dialogs:\n{str(e)}")
def open_new_project_dialog(self):
"""Öffnet Pdf-Projekt-Dialog."""
try:
@@ -4177,6 +4207,14 @@ class MainWindow(QMainWindow):
# Zeige Progressbar
self._show_transformation_progress_bar(len(jobs))
# Erfasse RAM-Verbrauch vor Transformation
import transform
if transform._saxon_worker_pool:
transform._saxon_worker_pool.capture_ram_before_transform()
if transform._fop_worker_pool:
transform._fop_worker_pool.capture_ram_before_transform()
# Starte Thread
self.transformation_thread.start()
@@ -4331,6 +4369,14 @@ class MainWindow(QMainWindow):
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
)
# Erfasse RAM-Verbrauch nach Transformation
import transform
if transform._saxon_worker_pool:
transform._saxon_worker_pool.capture_ram_after_transform()
if transform._fop_worker_pool:
transform._fop_worker_pool.capture_ram_after_transform()
# Verstecke Transformation-Progressbar
self._hide_transformation_progress_bar()
+250
View File
@@ -0,0 +1,250 @@
"""
Worker Pool Metriken-Dialog.
Zeigt Performance- und Ressourcenverbrauch-Metriken für Saxon- und FOP-Worker-Pools an.
"""
import logging
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QGroupBox,
QLabel,
QPushButton,
QTextEdit,
QTabWidget,
QWidget,
)
from PySide6.QtCore import Qt
logger = logging.getLogger(__name__)
class WorkerPoolMetricsDialog(QDialog):
"""
Dialog zur Anzeige von Worker-Pool-Metriken.
"""
def __init__(self, parent=None):
"""
Initialisiert den Metriken-Dialog.
Args:
parent: Eltern-Widget
"""
super().__init__(parent)
self.setWindowTitle("Worker Pool Performance-Metriken")
self.resize(800, 600)
self._setup_ui()
self._load_metrics()
def _setup_ui(self):
"""Erstellt die UI-Elemente."""
layout = QVBoxLayout(self)
# Tab-Widget für Saxon und FOP
self.tab_widget = QTabWidget()
layout.addWidget(self.tab_widget)
# Saxon-Tab
self.saxon_tab = self._create_metrics_tab("Saxon Worker Pool")
self.tab_widget.addTab(self.saxon_tab, "Saxon (XSLT)")
# FOP-Tab
self.fop_tab = self._create_metrics_tab("FOP Worker Pool")
self.tab_widget.addTab(self.fop_tab, "FOP (PDF)")
# Schließen-Button
button_layout = QHBoxLayout()
button_layout.addStretch()
close_button = QPushButton("Schließen")
close_button.clicked.connect(self.accept)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
def _create_metrics_tab(self, title: str) -> QWidget:
"""
Erstellt ein Tab-Widget für Metriken.
Args:
title: Titel der Metrik-Gruppe
Returns:
QWidget mit Metriken
"""
tab = QWidget()
layout = QVBoxLayout(tab)
# Status-Label
status_label = QLabel("Status: <i>Nicht initialisiert</i>")
status_label.setObjectName("status_label")
layout.addWidget(status_label)
# Kompilierungs-Metriken
compile_group = QGroupBox("Kompilierung")
compile_layout = QVBoxLayout(compile_group)
compile_time_label = QLabel("Kompilierungszeit: -")
compile_time_label.setObjectName("compile_time_label")
compile_layout.addWidget(compile_time_label)
layout.addWidget(compile_group)
# Worker-Start-Metriken
worker_start_group = QGroupBox("Worker-Start")
worker_start_layout = QVBoxLayout(worker_start_group)
worker_count_label = QLabel("Anzahl Worker: -")
worker_count_label.setObjectName("worker_count_label")
worker_start_layout.addWidget(worker_count_label)
total_start_time_label = QLabel("Summe Startzeit: -")
total_start_time_label.setObjectName("total_start_time_label")
worker_start_layout.addWidget(total_start_time_label)
avg_start_time_label = QLabel("Durchschnitt Startzeit: -")
avg_start_time_label.setObjectName("avg_start_time_label")
worker_start_layout.addWidget(avg_start_time_label)
layout.addWidget(worker_start_group)
# RAM-Metriken
ram_group = QGroupBox("Arbeitsspeicher (RAM)")
ram_layout = QVBoxLayout(ram_group)
ram_before_label = QLabel("RAM vor Transformation: -")
ram_before_label.setObjectName("ram_before_label")
ram_layout.addWidget(ram_before_label)
ram_before_avg_label = QLabel("RAM vor Transformation (Durchschnitt/Worker): -")
ram_before_avg_label.setObjectName("ram_before_avg_label")
ram_layout.addWidget(ram_before_avg_label)
ram_after_label = QLabel("RAM nach Transformation: -")
ram_after_label.setObjectName("ram_after_label")
ram_layout.addWidget(ram_after_label)
ram_after_avg_label = QLabel("RAM nach Transformation (Durchschnitt/Worker): -")
ram_after_avg_label.setObjectName("ram_after_avg_label")
ram_layout.addWidget(ram_after_avg_label)
ram_delta_label = QLabel("RAM-Zunahme: -")
ram_delta_label.setObjectName("ram_delta_label")
ram_layout.addWidget(ram_delta_label)
layout.addWidget(ram_group)
layout.addStretch()
return tab
def _load_metrics(self):
"""Lädt und zeigt die Metriken an."""
import transform
# Saxon Worker Pool
if transform._saxon_worker_pool:
self._update_metrics_tab(self.saxon_tab, transform._saxon_worker_pool, "Saxon Worker Pool")
else:
self._set_tab_status(self.saxon_tab, "<i>Nicht aktiviert</i>")
# FOP Worker Pool
if transform._fop_worker_pool:
self._update_metrics_tab(self.fop_tab, transform._fop_worker_pool, "FOP Worker Pool")
else:
self._set_tab_status(self.fop_tab, "<i>Nicht aktiviert</i>")
def _set_tab_status(self, tab: QWidget, status: str):
"""
Setzt den Status-Text eines Tabs.
Args:
tab: Das Tab-Widget
status: Der Status-Text
"""
status_label = tab.findChild(QLabel, "status_label")
if status_label:
status_label.setText(f"Status: {status}")
def _update_metrics_tab(self, tab: QWidget, pool, pool_name: str):
"""
Aktualisiert die Metriken in einem Tab.
Args:
tab: Das Tab-Widget
pool: Der Worker-Pool (Saxon oder FOP)
pool_name: Name des Pools
"""
metrics = pool.metrics
# Status
self._set_tab_status(tab, f"<b>Aktiviert</b> ({pool.num_workers} Worker)")
# Kompilierung
compile_time_label = tab.findChild(QLabel, "compile_time_label")
if compile_time_label:
compile_time_label.setText(f"Kompilierungszeit: <b>{metrics.compilation_time_seconds:.3f} s</b>")
# Worker-Start
worker_count_label = tab.findChild(QLabel, "worker_count_label")
if worker_count_label:
worker_count_label.setText(f"Anzahl Worker: <b>{len(metrics.worker_start_times)}</b>")
total_start_time_label = tab.findChild(QLabel, "total_start_time_label")
if total_start_time_label:
total_start_time_label.setText(
f"Summe Startzeit: <b>{metrics.total_worker_start_time_seconds:.3f} s</b>"
)
avg_start_time_label = tab.findChild(QLabel, "avg_start_time_label")
if avg_start_time_label:
avg_start_time_label.setText(
f"Durchschnitt Startzeit: <b>{metrics.average_worker_start_time_seconds:.3f} s/Worker</b>"
)
# RAM
ram_before_label = tab.findChild(QLabel, "ram_before_label")
if ram_before_label:
if metrics.total_ram_before_mb > 0:
ram_before_label.setText(f"RAM vor Transformation: <b>{metrics.total_ram_before_mb:.1f} MB</b>")
else:
ram_before_label.setText("RAM vor Transformation: <i>Noch nicht gemessen</i>")
ram_before_avg_label = tab.findChild(QLabel, "ram_before_avg_label")
if ram_before_avg_label:
if metrics.average_ram_before_mb > 0:
ram_before_avg_label.setText(
f"RAM vor Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_before_mb:.1f} MB</b>"
)
else:
ram_before_avg_label.setText(
"RAM vor Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
)
ram_after_label = tab.findChild(QLabel, "ram_after_label")
if ram_after_label:
if metrics.total_ram_after_mb > 0:
ram_after_label.setText(f"RAM nach Transformation: <b>{metrics.total_ram_after_mb:.1f} MB</b>")
else:
ram_after_label.setText("RAM nach Transformation: <i>Noch nicht gemessen</i>")
ram_after_avg_label = tab.findChild(QLabel, "ram_after_avg_label")
if ram_after_avg_label:
if metrics.average_ram_after_mb > 0:
ram_after_avg_label.setText(
f"RAM nach Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_after_mb:.1f} MB</b>"
)
else:
ram_after_avg_label.setText(
"RAM nach Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
)
ram_delta_label = tab.findChild(QLabel, "ram_delta_label")
if ram_delta_label:
if metrics.total_ram_before_mb > 0 and metrics.total_ram_after_mb > 0:
delta = metrics.total_ram_after_mb - metrics.total_ram_before_mb
delta_percent = (delta / metrics.total_ram_before_mb * 100) if metrics.total_ram_before_mb > 0 else 0
ram_delta_label.setText(f"RAM-Zunahme: <b>{delta:.1f} MB ({delta_percent:+.1f}%)</b>")
else:
ram_delta_label.setText("RAM-Zunahme: <i>Noch nicht gemessen</i>")
+62
View File
@@ -0,0 +1,62 @@
"""
Worker Pool Performance-Metriken.
Gemeinsame Datenstrukturen für Performance- und Ressourcenverbrauch-Metriken
der Worker-Pools (Saxon, FOP).
"""
from dataclasses import dataclass, field
@dataclass
class WorkerPoolMetrics:
"""
Metriken für Worker-Pool Performance und Ressourcenverbrauch.
"""
# Kompilierungszeit
compilation_time_seconds: float = 0.0
# Worker-Start-Zeiten
worker_start_times: list[float] = field(default_factory=list)
total_worker_start_time_seconds: float = 0.0
average_worker_start_time_seconds: float = 0.0
# RAM-Verbrauch (in MB)
ram_before_transform_mb_per_worker: list[float] = field(default_factory=list)
ram_after_transform_mb_per_worker: list[float] = field(default_factory=list)
total_ram_before_mb: float = 0.0
total_ram_after_mb: float = 0.0
average_ram_before_mb: float = 0.0
average_ram_after_mb: float = 0.0
# XSL-Kompilierungszeiten (nur für Saxon)
xsl_compilation_times: list[float] = field(default_factory=list)
total_xsl_compilation_time_seconds: float = 0.0
average_xsl_compilation_time_seconds: float = 0.0
def calculate_aggregates(self):
"""Berechnet aggregierte Werte (Summen, Durchschnitte)."""
# Worker-Start-Zeiten
if self.worker_start_times:
self.total_worker_start_time_seconds = sum(self.worker_start_times)
self.average_worker_start_time_seconds = self.total_worker_start_time_seconds / len(
self.worker_start_times
)
# RAM vor Transformation
if self.ram_before_transform_mb_per_worker:
self.total_ram_before_mb = sum(self.ram_before_transform_mb_per_worker)
self.average_ram_before_mb = self.total_ram_before_mb / len(self.ram_before_transform_mb_per_worker)
# RAM nach Transformation
if self.ram_after_transform_mb_per_worker:
self.total_ram_after_mb = sum(self.ram_after_transform_mb_per_worker)
self.average_ram_after_mb = self.total_ram_after_mb / len(self.ram_after_transform_mb_per_worker)
# XSL-Kompilierungszeiten
if self.xsl_compilation_times:
self.total_xsl_compilation_time_seconds = sum(self.xsl_compilation_times)
self.average_xsl_compilation_time_seconds = self.total_xsl_compilation_time_seconds / len(
self.xsl_compilation_times
)
Generated
+30
View File
@@ -38,6 +38,7 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "polars", extra = ["connectorx", "pyarrow"] },
{ name = "psutil" },
{ name = "pydantic-settings" },
{ name = "pydantic-yaml" },
{ name = "pyqtdarktheme" },
@@ -52,6 +53,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
{ name = "psutil", specifier = ">=6.1.1" },
{ name = "pydantic-settings", specifier = ">=2.9.1" },
{ name = "pydantic-yaml", specifier = ">=1.5.1" },
{ name = "pyqtdarktheme", specifier = ">=2.1.0" },
@@ -83,6 +85,34 @@ pyarrow = [
{ name = "pyarrow" },
]
[[package]]
name = "psutil"
version = "7.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" },
{ url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" },
{ url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" },
{ url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" },
{ url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" },
{ url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" },
{ url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" },
{ url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" },
{ url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" },
{ url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" },
{ url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" },
{ url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" },
{ url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" },
{ url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" },
{ url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" },
{ url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" },
]
[[package]]
name = "pyarrow"
version = "21.0.0"