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:
@@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"pyside6>=6.9.1",
|
"pyside6>=6.9.1",
|
||||||
"polars[connectorx,pyarrow]>=1.31.0",
|
"polars[connectorx,pyarrow]>=1.31.0",
|
||||||
"pydantic-yaml>=1.5.1",
|
"pydantic-yaml>=1.5.1",
|
||||||
|
"psutil>=6.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
+78
-4
@@ -8,11 +8,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import psutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from worker_metrics import WorkerPoolMetrics
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
||||||
@@ -206,6 +210,9 @@ class FopWorkerPool:
|
|||||||
# Classpath für FOP
|
# Classpath für FOP
|
||||||
self.fop_classpath: Optional[str] = None
|
self.fop_classpath: Optional[str] = None
|
||||||
|
|
||||||
|
# Performance-Metriken
|
||||||
|
self.metrics = WorkerPoolMetrics()
|
||||||
|
|
||||||
# Initialisierung
|
# Initialisierung
|
||||||
self._build_fop_classpath()
|
self._build_fop_classpath()
|
||||||
self._compile_worker_class()
|
self._compile_worker_class()
|
||||||
@@ -236,6 +243,7 @@ class FopWorkerPool:
|
|||||||
|
|
||||||
def _compile_worker_class(self):
|
def _compile_worker_class(self):
|
||||||
"""Kompiliert die FopWorker-Java-Klasse."""
|
"""Kompiliert die FopWorker-Java-Klasse."""
|
||||||
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Erstelle temporäres Verzeichnis
|
# Erstelle temporäres Verzeichnis
|
||||||
self.temp_dir = Path(tempfile.mkdtemp(prefix="fop_worker_"))
|
self.temp_dir = Path(tempfile.mkdtemp(prefix="fop_worker_"))
|
||||||
@@ -261,7 +269,12 @@ class FopWorkerPool:
|
|||||||
|
|
||||||
self.worker_class_path = self.temp_dir
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Kompilieren von FopWorker: {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)
|
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for i in range(self.num_workers):
|
for i in range(self.num_workers):
|
||||||
|
worker_start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Starte JVM-Prozess mit FopWorker
|
# Starte JVM-Prozess mit FopWorker
|
||||||
# Übergebe fop.xconf als Argument falls vorhanden
|
# Ü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})")
|
logger.debug(f"FOP Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
|
||||||
|
|
||||||
# Warte kurz damit Worker initialisieren kann
|
# Warte kurz damit Worker initialisieren kann
|
||||||
import time
|
|
||||||
|
|
||||||
time.sleep(0.2) # FOP braucht etwas länger zum Initialisieren
|
time.sleep(0.2) # FOP braucht etwas länger zum Initialisieren
|
||||||
|
|
||||||
# Prüfe ob Worker noch läuft
|
# 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}"
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Starten von FOP Worker {i}: {e}")
|
logger.error(f"Fehler beim Starten von FOP Worker {i}: {e}")
|
||||||
raise
|
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]:
|
def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
@@ -410,6 +433,57 @@ class FopWorkerPool:
|
|||||||
# Gebe Worker-Lock frei
|
# Gebe Worker-Lock frei
|
||||||
self.worker_locks[worker_idx].release()
|
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):
|
def shutdown(self):
|
||||||
"""Beendet alle Worker-Prozesse sauber."""
|
"""Beendet alle Worker-Prozesse sauber."""
|
||||||
logger.info("Beende FOP-Worker-Pool...")
|
logger.info("Beende FOP-Worker-Pool...")
|
||||||
|
|||||||
+78
-4
@@ -8,11 +8,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheina
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import psutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from worker_metrics import WorkerPoolMetrics
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
||||||
@@ -197,6 +201,9 @@ class SaxonWorkerPool:
|
|||||||
self.worker_class_path: Optional[Path] = None
|
self.worker_class_path: Optional[Path] = None
|
||||||
self.worker_log_dir: Optional[Path] = None
|
self.worker_log_dir: Optional[Path] = None
|
||||||
|
|
||||||
|
# Performance-Metriken
|
||||||
|
self.metrics = WorkerPoolMetrics()
|
||||||
|
|
||||||
# Initialisierung
|
# Initialisierung
|
||||||
self._compile_worker_class()
|
self._compile_worker_class()
|
||||||
self._start_workers()
|
self._start_workers()
|
||||||
@@ -205,6 +212,7 @@ class SaxonWorkerPool:
|
|||||||
|
|
||||||
def _compile_worker_class(self):
|
def _compile_worker_class(self):
|
||||||
"""Kompiliert die SaxonWorker-Java-Klasse."""
|
"""Kompiliert die SaxonWorker-Java-Klasse."""
|
||||||
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Erstelle temporäres Verzeichnis
|
# Erstelle temporäres Verzeichnis
|
||||||
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
|
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
|
||||||
@@ -242,7 +250,12 @@ class SaxonWorkerPool:
|
|||||||
|
|
||||||
self.worker_class_path = self.temp_dir
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Kompilieren von SaxonWorker: {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)
|
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for i in range(self.num_workers):
|
for i in range(self.num_workers):
|
||||||
|
worker_start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Starte JVM-Prozess mit SaxonWorker
|
# Starte JVM-Prozess mit SaxonWorker
|
||||||
cmd = [str(self.java_vm_path), "-cp", full_classpath, "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})")
|
logger.debug(f"Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
|
||||||
|
|
||||||
# Warte kurz damit Worker initialisieren kann
|
# Warte kurz damit Worker initialisieren kann
|
||||||
import time
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Prüfe ob Worker noch läuft
|
# 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}"
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
|
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
|
||||||
raise
|
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(
|
def transform(
|
||||||
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
|
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
|
# Gebe Worker-Lock frei
|
||||||
self.worker_locks[worker_idx].release()
|
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):
|
def shutdown(self):
|
||||||
"""Beendet alle Worker-Prozesse sauber."""
|
"""Beendet alle Worker-Prozesse sauber."""
|
||||||
logger.info("Beende Saxon-Worker-Pool...")
|
logger.info("Beende Saxon-Worker-Pool...")
|
||||||
|
|||||||
+79
-4
@@ -9,11 +9,15 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheina
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import psutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from worker_metrics import WorkerPoolMetrics
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Java-Worker-Code für s9api (wird zur Laufzeit kompiliert)
|
# 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_class_path: Optional[Path] = None
|
||||||
self.worker_log_dir: Optional[Path] = None
|
self.worker_log_dir: Optional[Path] = None
|
||||||
|
|
||||||
|
# Performance-Metriken
|
||||||
|
self.metrics = WorkerPoolMetrics()
|
||||||
|
|
||||||
# Initialisierung
|
# Initialisierung
|
||||||
self._compile_worker_class()
|
self._compile_worker_class()
|
||||||
self._start_workers()
|
self._start_workers()
|
||||||
@@ -187,6 +194,7 @@ class SaxonWorkerPoolS9Api:
|
|||||||
|
|
||||||
def _compile_worker_class(self):
|
def _compile_worker_class(self):
|
||||||
"""Kompiliert die SaxonS9ApiWorker-Java-Klasse."""
|
"""Kompiliert die SaxonS9ApiWorker-Java-Klasse."""
|
||||||
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Erstelle temporäres Verzeichnis
|
# Erstelle temporäres Verzeichnis
|
||||||
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_s9api_worker_"))
|
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_s9api_worker_"))
|
||||||
@@ -224,7 +232,13 @@ class SaxonWorkerPoolS9Api:
|
|||||||
|
|
||||||
self.worker_class_path = self.temp_dir
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Kompilieren von SaxonS9ApiWorker: {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)
|
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for i in range(self.num_workers):
|
for i in range(self.num_workers):
|
||||||
|
worker_start_time = time.time()
|
||||||
try:
|
try:
|
||||||
# Starte JVM-Prozess mit SaxonS9ApiWorker
|
# Starte JVM-Prozess mit SaxonS9ApiWorker
|
||||||
cmd = [str(self.java_vm_path), "-cp", full_classpath, "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})")
|
logger.debug(f"S9Api Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
|
||||||
|
|
||||||
# Warte kurz damit Worker initialisieren kann
|
# Warte kurz damit Worker initialisieren kann
|
||||||
import time
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Prüfe ob Worker noch läuft
|
# 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}"
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Starten von S9Api Worker {i}: {e}")
|
logger.error(f"Fehler beim Starten von S9Api Worker {i}: {e}")
|
||||||
raise
|
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(
|
def transform(
|
||||||
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
|
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
|
# Gebe Worker-Lock frei
|
||||||
self.worker_locks[worker_idx].release()
|
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):
|
def shutdown(self):
|
||||||
"""Beendet alle Worker-Prozesse sauber."""
|
"""Beendet alle Worker-Prozesse sauber."""
|
||||||
logger.info("Beende Saxon-S9Api-Worker-Pool...")
|
logger.info("Beende Saxon-S9Api-Worker-Pool...")
|
||||||
|
|||||||
@@ -1078,6 +1078,25 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
|
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
|
||||||
self.ui.actionEinstellungen.triggered.connect(self.open_settings_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
|
# Button "lade aus FN2" verbinden
|
||||||
self.ui.pB_lade_aus_fn2.clicked.connect(self.on_load_from_fn2_clicked)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {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):
|
def open_new_project_dialog(self):
|
||||||
"""Öffnet Pdf-Projekt-Dialog."""
|
"""Öffnet Pdf-Projekt-Dialog."""
|
||||||
try:
|
try:
|
||||||
@@ -4177,6 +4207,14 @@ class MainWindow(QMainWindow):
|
|||||||
# Zeige Progressbar
|
# Zeige Progressbar
|
||||||
self._show_transformation_progress_bar(len(jobs))
|
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
|
# Starte Thread
|
||||||
self.transformation_thread.start()
|
self.transformation_thread.start()
|
||||||
|
|
||||||
@@ -4331,6 +4369,14 @@ class MainWindow(QMainWindow):
|
|||||||
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
|
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
|
# Verstecke Transformation-Progressbar
|
||||||
self._hide_transformation_progress_bar()
|
self._hide_transformation_progress_bar()
|
||||||
|
|
||||||
|
|||||||
@@ -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>")
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -38,6 +38,7 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "polars", extra = ["connectorx", "pyarrow"] },
|
{ name = "polars", extra = ["connectorx", "pyarrow"] },
|
||||||
|
{ name = "psutil" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pydantic-yaml" },
|
{ name = "pydantic-yaml" },
|
||||||
{ name = "pyqtdarktheme" },
|
{ name = "pyqtdarktheme" },
|
||||||
@@ -52,6 +53,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
|
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
|
||||||
|
{ name = "psutil", specifier = ">=6.1.1" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
||||||
{ name = "pydantic-yaml", specifier = ">=1.5.1" },
|
{ name = "pydantic-yaml", specifier = ">=1.5.1" },
|
||||||
{ name = "pyqtdarktheme", specifier = ">=2.1.0" },
|
{ name = "pyqtdarktheme", specifier = ">=2.1.0" },
|
||||||
@@ -83,6 +85,34 @@ pyarrow = [
|
|||||||
{ name = "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]]
|
[[package]]
|
||||||
name = "pyarrow"
|
name = "pyarrow"
|
||||||
version = "21.0.0"
|
version = "21.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user