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
+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>")