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:
+78
-4
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user