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