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