Performance: 4x schnellere XSLT-Transformationen durch Worker-Pool

Problem: 82 XML-Dateien brauchten 160 Sekunden (JVM-Startup-Overhead)

Lösung: Persistente JVM-Worker-Prozesse mit JAXP Transformer API
- Saxon Worker Pool mit N persistenten JVM-Prozessen
- Eliminiert JVM-Startup und Classpath-Scanning bei jedem Job
- Parallele Verarbeitung mit ThreadPoolExecutor
- JAXP Transformer API (javax.xml.transform) - stabil, kein System.exit()
- Konfigurierbare Worker-Anzahl über Performance-Menü

Ergebnis: 82 Dateien in 40 Sekunden (4x Speedup, ~0.49s pro Datei)

Zusätzliche Verbesserungen:
- Dual-Logging (Datei + Konsole) mit Timestamps
- Worker-stderr-Logs in Projektverzeichnis/temp/
- Umfangreiche Debug-Ausgaben für Fehlerdiagnose
- Robuste Fehlerbehandlung mit ErrorListener

Technische Details:
- SaxonWorkerPool: Verwaltet N Worker-Prozesse
- JAXP statt Transform.main() (kein System.exit!)
- Worker-Locks für thread-sichere Job-Verteilung
- Graceful Shutdown mit EXIT-Befehl
- Fallback auf subprocess bei Pool-Fehlern

Dateien:
- src/saxon_pool.py (NEU): Worker-Pool-Implementation
- src/transform.py: Integration mit Worker-Pool
- src/ui/MainWindow.py: Pool-Initialisierung, Performance-Menü
- src/conf.py: max_workers Einstellung
- src/main.py: Dual-Logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 16:46:39 +01:00
parent 055428e8cf
commit d0cdcd6432
5 changed files with 719 additions and 58 deletions
+70 -19
View File
@@ -11,10 +11,26 @@ import logging
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Any
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from saxon_pool import SaxonWorkerPool
logger = logging.getLogger(__name__)
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
_saxon_worker_pool: Optional["SaxonWorkerPool"] = None
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]):
"""Setzt den globalen Saxon-Worker-Pool."""
global _saxon_worker_pool
_saxon_worker_pool = pool
if pool:
logger.info(f"Saxon-Worker-Pool aktiviert mit {pool.num_workers} Workern")
else:
logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)")
class TransformationJob:
"""
@@ -23,6 +39,9 @@ class TransformationJob:
Ähnlich zur TestFall-Klasse in validate-xls.py, aber für DocuMentor angepasst.
"""
# Klassenweiter Cache für Saxon-Classpaths (Performance-Optimierung)
_classpath_cache: dict[Path, str] = {}
def __init__(
self,
project_dir: Path,
@@ -161,30 +180,63 @@ class TransformationJob:
logger.error(error_msg)
return False, error_msg
logger.info(f"Starte Saxon-Transformation: {self.xml_file.name}")
# Versuche zuerst den Worker-Pool zu nutzen (schneller!)
global _saxon_worker_pool
if _saxon_worker_pool:
try:
success, message = _saxon_worker_pool.transform(
source_xml=xml_abs,
xsl_stylesheet=self.xsl_file,
output_fo=self.temp_fo,
xslt_params=self.xslt_params,
)
if success:
logger.info(f"Saxon-Transformation erfolgreich (Worker-Pool): {self.xml_file.name}")
else:
logger.error(f"Saxon-Transformation fehlgeschlagen (Worker-Pool): {message}")
return success, message
except Exception as e:
logger.warning(f"Worker-Pool-Fehler, Fallback auf subprocess: {e}")
# Fallback auf subprocess unten
# Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster)
# XSLT-Parameter formatieren
params = [f"{key}={value}" for key, value in self.xslt_params.items()]
# Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath
import glob
# Hole Classpath aus Cache oder erstelle ihn
saxon_dir = self.saxon_jar_path.parent
all_jars = glob.glob(str(saxon_dir / "*.jar"))
if saxon_dir not in TransformationJob._classpath_cache:
# Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath
import glob
# Sammle auch alle JARs aus dem lib-Unterordner (z.B. xmlresolver)
lib_dir = saxon_dir / "lib"
if lib_dir.exists() and lib_dir.is_dir():
lib_jars = glob.glob(str(lib_dir / "*.jar"))
all_jars.extend(lib_jars)
logger.debug(f"Zusätzliche JARs aus lib-Verzeichnis gefunden: {len(lib_jars)}")
all_jars = glob.glob(str(saxon_dir / "*.jar"))
# Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows)
import sys
# Sammle auch alle JARs aus dem lib-Unterordner (z.B. xmlresolver)
lib_dir = saxon_dir / "lib"
if lib_dir.exists() and lib_dir.is_dir():
lib_jars = glob.glob(str(lib_dir / "*.jar"))
all_jars.extend(lib_jars)
logger.debug(f"Zusätzliche JARs aus lib-Verzeichnis gefunden: {len(lib_jars)}")
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows)
import sys
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Cache den Classpath für zukünftige Jobs
TransformationJob._classpath_cache[saxon_dir] = classpath
logger.debug(f"Classpath für {saxon_dir} gecacht")
else:
classpath = TransformationJob._classpath_cache[saxon_dir]
logger.debug("Classpath aus Cache verwendet")
# Saxon-Kommandozeile
# Verwende -cp mit allen JARs und rufe Transform-Main direkt auf
cmd_line = [
str(self.java_vm_path),
"-cp",
@@ -196,8 +248,7 @@ class TransformationJob:
*params,
]
logger.info(f"Starte Saxon-Transformation: {self.xml_file.name}")
logger.debug(f"Kommandozeile: {' '.join(cmd_line)}")
logger.debug(f"Kommandozeile (subprocess fallback): {' '.join(cmd_line)}")
try:
result = subprocess.run(
@@ -214,7 +265,7 @@ class TransformationJob:
logger.debug(f"Saxon StdErr:\n{result.stderr}")
if result.returncode == 0:
logger.info(f"Saxon-Transformation erfolgreich: {self.xml_file.name}")
logger.info(f"Saxon-Transformation erfolgreich (subprocess): {self.xml_file.name}")
return True, "Erfolgreich"
else:
error_msg = (