From 0f502fcdd26e385ea7bf561dcc9dd78c303a0851 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Tue, 5 Aug 2025 01:01:13 +0200 Subject: [PATCH 1/4] Fixed a javadoc issue. --- src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java index b32d9fb..0d435da 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java @@ -355,7 +355,7 @@ default public Markup renderFragmentInvocation(final String name, final Set Date: Tue, 5 Aug 2025 01:21:48 +0200 Subject: [PATCH 2/4] Fixed an issue with translator context tracking. --- .../java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java index 169678f..3df888f 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -359,6 +359,7 @@ public void enterTemplateDefinition(TemplateDefinitionContext ctx) { @Override public void enterTemplateFile(TemplateFileContext ctx) { assert blockStack.isEmpty() : UNEXPECTED_INDENTATION; + this.translatorContext.setCurrentSection(TemplateSection.DEFAULT); } @Override From 0eae5778e9e240b1d28e358fc8b611db8ed1957b Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Tue, 5 Aug 2025 01:49:05 +0200 Subject: [PATCH 3/4] TEDEFO-4559-4561: Enhance EFX template translation with profiling options and reporting. - Updated EfxTranslator and EfxTemplateTranslator interfaces to accept TranslatorOptions for profiling. - Introduced EfxProfilerReportGenerator for generating HTML profiling reports. - Added TranslatorTimings class to hold timing measurements for processing phases. - Enhanced EfxTemplateTranslatorV2 to support profiling and logging of translation times. - Updated EfxTranslatorOptions to include profiling settings. --- .../java/eu/europa/ted/efx/EfxTranslator.java | 8 +- .../europa/ted/efx/EfxTranslatorOptions.java | 43 +++- .../efx/interfaces/EfxTemplateTranslator.java | 43 +++- .../ted/efx/interfaces/TranslatorOptions.java | 16 ++ .../ted/efx/sdk1/EfxTemplateTranslatorV1.java | 20 +- .../ted/efx/sdk2/EfxTemplateTranslatorV2.java | 72 ++++++- .../efx/util/EfxProfilerReportGenerator.java | 190 ++++++++++++++++++ .../ted/efx/util/TranslatorTimings.java | 60 ++++++ 8 files changed, 423 insertions(+), 29 deletions(-) create mode 100644 src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java create mode 100644 src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslator.java b/src/main/java/eu/europa/ted/efx/EfxTranslator.java index 498b548..5a4cf6d 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslator.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslator.java @@ -84,7 +84,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende final Path pathname, TranslatorOptions options) throws IOException, InstantiationException { return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) - .renderTemplate(pathname); + .renderTemplate(pathname, options); } public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, @@ -98,7 +98,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende final Path pathname, TranslatorOptions options) throws IOException, InstantiationException { return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) - .renderTemplate(pathname); + .renderTemplate(pathname, options); } /** @@ -130,7 +130,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende final String qualifier, final String template, TranslatorOptions options) throws InstantiationException { return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) - .renderTemplate(template); + .renderTemplate(template, options); } /** @@ -164,7 +164,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende final String qualifier, final InputStream stream, TranslatorOptions options) throws IOException, InstantiationException { return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) - .renderTemplate(stream); + .renderTemplate(stream, options); } //#endregion Translate EFX templates ---------------------------------------- diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java index 225eeba..3c62202 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java @@ -1,5 +1,6 @@ package eu.europa.ted.efx; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -19,39 +20,59 @@ public class EfxTranslatorOptions implements TranslatorOptions { */ public static final String DEFAULT_UDF_NAMESPACE = "efx-udf"; + /** + * Default value for EFX profiling enablement. + * By default, profiling is disabled for performance reasons. + */ + public static final boolean DEFAULT_PROFILER_ENABLED = false; + + /** + * Default value for EFX profiling output path. + * By default, no profiling output file is generated. + */ + public static final Path DEFAULT_PROFILER_OUTPUT_PATH = null; + // Change to EfxDecimalFormatSymbols.EFX_DEFAULT to use the decimal format // preferred by OP (space as thousands separator and comma as decimal separator). - public static final EfxTranslatorOptions DEFAULT = new EfxTranslatorOptions(DEFAULT_UDF_NAMESPACE, DecimalFormat.XSL_DEFAULT, Locale.ENGLISH); + public static final EfxTranslatorOptions DEFAULT = new EfxTranslatorOptions(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, DEFAULT_UDF_NAMESPACE, DecimalFormat.XSL_DEFAULT, Locale.ENGLISH); private final DecimalFormat symbols; private final Locale primaryLocale; private final ArrayList otherLocales; private final String userDefinedFunctionNamespace; + private final boolean profilerEnabled; + private final Path profilerOutputPath; public EfxTranslatorOptions(DecimalFormat symbols) { - this(DEFAULT_UDF_NAMESPACE, symbols); + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, DEFAULT_UDF_NAMESPACE, symbols, Locale.ENGLISH); } public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols) { - this(udfNamespace, symbols, Locale.ENGLISH); + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, udfNamespace, symbols, Locale.ENGLISH); } public EfxTranslatorOptions(DecimalFormat symbols, String primaryLanguage, String... otherLanguages) { - this(symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, DEFAULT_UDF_NAMESPACE, symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); } public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols, String primaryLanguage, String... otherLanguages) { - this(udfNamespace, symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, udfNamespace, symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); } public EfxTranslatorOptions(DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { - this(DEFAULT_UDF_NAMESPACE, symbols, primaryLocale, otherLocales); + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, DEFAULT_UDF_NAMESPACE, symbols, primaryLocale, otherLocales); } public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { + this(DEFAULT_PROFILER_ENABLED, DEFAULT_PROFILER_OUTPUT_PATH, udfNamespace, symbols, primaryLocale, otherLocales); + } + + public EfxTranslatorOptions(boolean profilerEnabled, Path profilerOutputPath, String udfNamespace, DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { this.userDefinedFunctionNamespace = udfNamespace; this.symbols = symbols; this.primaryLocale = primaryLocale; + this.profilerEnabled = profilerEnabled; + this.profilerOutputPath = profilerOutputPath; this.otherLocales = new ArrayList<>(Arrays.asList(otherLocales)); } @@ -99,4 +120,14 @@ public String[] getAllLanguage3LetterCodes() { public String getUserDefinedFunctionNamespace() { return this.userDefinedFunctionNamespace; } + + @Override + public boolean isProfilerEnabled() { + return this.profilerEnabled; + } + + @Override + public Path getProfilerOutputPath() { + return this.profilerOutputPath; + } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/interfaces/EfxTemplateTranslator.java b/src/main/java/eu/europa/ted/efx/interfaces/EfxTemplateTranslator.java index 430eccd..1fb49f7 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/EfxTemplateTranslator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/EfxTemplateTranslator.java @@ -17,6 +17,8 @@ import java.io.InputStream; import java.nio.file.Path; +import eu.europa.ted.efx.EfxTranslatorOptions; + /** * Defines the API of an EFX template translator. * @@ -25,6 +27,16 @@ */ public interface EfxTemplateTranslator extends EfxExpressionTranslator { + /** + * Translate the EFX template stored in a file, given the pathname of the file. + * + * @param pathname The path and filename of the EFX template file to translate. + * @param options The options to be used by the EFX template translator. + * @return A string containing the translated template. + * @throws IOException If the file cannot be read. + */ + String renderTemplate(Path pathname, TranslatorOptions options) throws IOException; + /** * Translate the EFX template stored in a file, given the pathname of the file. * @@ -32,7 +44,18 @@ public interface EfxTemplateTranslator extends EfxExpressionTranslator { * @return A string containing the translated template. * @throws IOException If the file cannot be read. */ - String renderTemplate(Path pathname) throws IOException; + default String renderTemplate(Path pathname) throws IOException { + return renderTemplate(pathname, EfxTranslatorOptions.DEFAULT); + } + + /** + * Translate the EFX template stored in the given string. + * + * @param template A string containing an EFX template to be translated. + * @param options The options to be used by the EFX template translator. + * @return A string containing the translated template. + */ + String renderTemplate(String template, TranslatorOptions options); /** * Translate the EFX template stored in the given string. @@ -40,7 +63,19 @@ public interface EfxTemplateTranslator extends EfxExpressionTranslator { * @param template A string containing an EFX template to be translated. * @return A string containing the translated template. */ - String renderTemplate(String template); + default String renderTemplate(String template) { + return renderTemplate(template, EfxTranslatorOptions.DEFAULT); + } + + /** + * Translate the EFX template given as an InputStream. + * + * @param stream An InputStream with the EFX template to be translated. + * @param options The options to be used by the EFX template translator. + * @return A string containing the translated template. + * @throws IOException If the InputStream cannot be read. + */ + String renderTemplate(InputStream stream, TranslatorOptions options) throws IOException; /** * Translate the EFX template given as an InputStream. @@ -49,5 +84,7 @@ public interface EfxTemplateTranslator extends EfxExpressionTranslator { * @return A string containing the translated template. * @throws IOException If the InputStream cannot be read. */ - String renderTemplate(InputStream stream) throws IOException; + default String renderTemplate(InputStream stream) throws IOException { + return renderTemplate(stream, EfxTranslatorOptions.DEFAULT); + } } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java index 1cf63d2..5be3311 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java @@ -1,5 +1,7 @@ package eu.europa.ted.efx.interfaces; +import java.nio.file.Path; + import eu.europa.ted.efx.model.DecimalFormat; public interface TranslatorOptions { @@ -14,4 +16,18 @@ public interface TranslatorOptions { public String[] getAllLanguage3LetterCodes(); public String getUserDefinedFunctionNamespace(); + + /** + * Returns whether EFX profiling is enabled for performance analysis. + * + * @return true if EFX profiling should be enabled, false otherwise + */ + public boolean isProfilerEnabled(); + + /** + * Returns the output path for EFX profiling results. + * + * @return Path where profiling results should be written, or null if no file output is desired + */ + public Path getProfilerOutputPath(); } diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java index 9cf4481..1392d12 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -23,6 +23,7 @@ import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.interfaces.TranslatorContext; +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; @@ -125,27 +126,30 @@ public EfxTemplateTranslatorV1(final MarkupGenerator markupGenerator, * Opens the indicated EFX file and translates the EFX template it contains. */ @Override - public String renderTemplate(final Path pathname) throws IOException { - - return renderTemplate(CharStreams.fromPath(pathname)); + public String renderTemplate(final Path pathname, TranslatorOptions options) throws IOException { + return renderTemplate(CharStreams.fromPath(pathname), options); } /** * Translates the template contained in the string passed as a parameter. */ @Override - public String renderTemplate(final String template) { - return renderTemplate(CharStreams.fromString(template)); + public String renderTemplate(final String template, TranslatorOptions options) { + return renderTemplate(CharStreams.fromString(template), options); } @Override - public String renderTemplate(final InputStream stream) throws IOException { - return renderTemplate(CharStreams.fromStream(stream)); + public String renderTemplate(final InputStream stream, TranslatorOptions options) throws IOException { + return renderTemplate(CharStreams.fromStream(stream), options); } - private String renderTemplate(final CharStream charStream) { + private String renderTemplate(final CharStream charStream, TranslatorOptions options) { logger.debug("Rendering template"); + if (options != null && options.isProfilerEnabled()) { + logger.warn("EFX profiling is not available for EFX-1 templates. No profiler output will be generated."); + } + final EfxLexer lexer = new EfxLexer(charStream); final CommonTokenStream tokens = new CommonTokenStream(lexer); final EfxParser parser = new EfxParser(tokens); diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java index 3df888f..ed0cdf5 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -14,6 +14,9 @@ import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.atn.DecisionInfo; +import org.antlr.v4.runtime.atn.ParseInfo; +import org.antlr.v4.runtime.atn.ProfilingATNSimulator; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.slf4j.Logger; @@ -28,9 +31,12 @@ import eu.europa.ted.efx.interfaces.MarkupGenerator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.interfaces.TemplateSection; import eu.europa.ted.efx.interfaces.TranslatorContext; import eu.europa.ted.efx.model.Context; +import eu.europa.ted.efx.util.EfxProfilerReportGenerator; +import eu.europa.ted.efx.util.TranslatorTimings; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.expressions.Expression; @@ -196,36 +202,45 @@ public EfxTemplateTranslatorV2(final MarkupGenerator markupGenerator, * Opens the indicated EFX file and translates the EFX template it contains. */ @Override - public String renderTemplate(final Path pathname) throws IOException { - - return renderTemplate(CharStreams.fromPath(pathname)); + public String renderTemplate(final Path pathname, TranslatorOptions options) throws IOException { + return renderTemplate(CharStreams.fromPath(pathname), options); } /** * Translates the template contained in the string passed as a parameter. */ @Override - public String renderTemplate(final String template) { - return renderTemplate(CharStreams.fromString(template)); + public String renderTemplate(final String template, TranslatorOptions options) { + return renderTemplate(CharStreams.fromString(template), options); } @Override - public String renderTemplate(final InputStream stream) throws IOException { - return renderTemplate(CharStreams.fromStream(stream)); + public String renderTemplate(final InputStream stream, TranslatorOptions options) throws IOException { + return renderTemplate(CharStreams.fromStream(stream), options); } - private String renderTemplate(final CharStream charStream) { + private String renderTemplate(final CharStream charStream, TranslatorOptions options) { logger.debug("Rendering template"); + final long startTime = System.currentTimeMillis(); // New in EFX-2: template preprocessing + final long preprocessingStartTime = System.currentTimeMillis(); final TemplatePreprocessor preprocessor = this.new TemplatePreprocessor(charStream); final String preprocessedTemplate = preprocessor.processTemplate(); + final long preprocessingEndTime = System.currentTimeMillis(); + final long preprocessingDuration = preprocessingEndTime - preprocessingStartTime; // Now parse the preprocessed template + final long translationStartTime = System.currentTimeMillis(); final EfxLexer lexer = new EfxLexer(CharStreams.fromString(preprocessedTemplate)); final CommonTokenStream tokens = new CommonTokenStream(lexer); final EfxParser parser = new EfxParser(tokens); + // Enable profiling if requested + if (options != null && options.isProfilerEnabled()) { + parser.setInterpreter(new ProfilingATNSimulator(parser)); + } + if (errorListener != null) { lexer.removeErrorListeners(); lexer.addErrorListener(errorListener); @@ -237,6 +252,18 @@ private String renderTemplate(final CharStream charStream) { final ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(this, tree); + + final long translationEndTime = System.currentTimeMillis(); + final long translationDuration = translationEndTime - translationStartTime; + + final long endTime = System.currentTimeMillis(); + final long totalDuration = endTime - startTime; + + // Log profiling information if enabled + if (options != null && options.isProfilerEnabled()) { + TranslatorTimings timingData = new TranslatorTimings(preprocessingDuration, translationDuration, totalDuration); + this.generateAndSaveProfilerReport(parser, options.getProfilerOutputPath(), timingData); + } logger.debug("Finished rendering template"); @@ -264,6 +291,35 @@ private String getTranslatedMarkup() { return sb.toString().trim(); } + /** + * Logs ANTLR4 profiling results showing which grammar rules took the most time. + * Only active when profiling is enabled via system property. + * + * @param parser The EfxParser instance to extract profiling data from + * @param profilingOutputPath Path where the HTML report should be saved + * @param timingData Timing measurements for different processing phases + */ + private void generateAndSaveProfilerReport(final EfxParser parser, final Path profilingOutputPath, final TranslatorTimings timingData) { + final ParseInfo parseInfo = parser.getParseInfo(); + if (parseInfo == null) { + logger.warn("ParseInfo not available - profiling may not be enabled in parser"); + return; + } + + final DecisionInfo[] decisions = parseInfo.getDecisionInfo(); + + // Sort decisions by time descending + java.util.Arrays.sort(decisions, (a, b) -> Long.compare(b.timeInPrediction, a.timeInPrediction)); + + long totalTime = java.util.Arrays.stream(decisions) + .mapToLong(d -> d.timeInPrediction) + .sum(); + + // Write HTML report to file if path is provided + EfxProfilerReportGenerator.generateAndSaveProfilerReport(parser, decisions, totalTime, timingData, profilingOutputPath); + } + + // #region Global declarationExpressions --------------------------------------- @Override diff --git a/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java b/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java new file mode 100644 index 0000000..47c3e49 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java @@ -0,0 +1,190 @@ +package eu.europa.ted.efx.util; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.antlr.v4.runtime.atn.DecisionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import eu.europa.ted.efx.sdk2.EfxParser; + +/** + * Utility class for generating EFX parser profiling reports. + * This class provides methods to generate HTML reports from ANTLR profiling + * data. + */ +public class EfxProfilerReportGenerator { + + private static final Logger logger = LoggerFactory.getLogger(EfxProfilerReportGenerator.class); + + private EfxProfilerReportGenerator() { + // Utility class - prevent instantiation + } + + /** + * Generates and saves an HTML profiling report to the specified file path. + * + * @param parser The EFX parser that was profiled + * @param decisions Array of decision information from profiling + * @param totalTime Total parsing time in nanoseconds + * @param timingData Timing measurements for different processing phases + * @param outputPath Path where the HTML report should be saved + */ + public static void generateAndSaveProfilerReport(final EfxParser parser, final DecisionInfo[] decisions, + final long totalTime, final TranslatorTimings timingData, final Path outputPath) { + if (outputPath == null) { + logger.debug("No output path provided for profiling report, skipping file generation"); + return; + } + + try { + Files.createDirectories(outputPath.getParent()); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputPath))) { + writer.print(generateProfilerReport(parser, decisions, totalTime, timingData)); + } + logger.info("EFX profiling results saved to: {}", outputPath); + } catch (IOException e) { + logger.error("Failed to write EFX profiling results to file: {}", outputPath, e); + } + } + + /** + * Generates an HTML profiling report as a string. + * + * @param parser The EFX parser that was profiled + * @param decisions Array of decision information from profiling + * @param totalTime Total parsing time in nanoseconds + * @param timingData Timing measurements for different processing phases + * @return HTML report as a string + */ + public static String generateProfilerReport(final EfxParser parser, final DecisionInfo[] decisions, + final long totalTime, final TranslatorTimings timingData) { + StringBuilder html = new StringBuilder(); + + // HTML structure with CSS similar to XSLT profiling + html.append("\n"); + html.append("\n"); + html.append("\n"); + html.append(" \n"); + html.append(" EFX Parser Profiling Results\n"); + html.append(" \n"); + html.append("\n"); + html.append("\n"); + + // Header + html.append("

EFX Parser Profiling Results

\n"); + + // Summary section + html.append("
\n"); + html.append("

Summary

\n"); + html.append("

Total Grammar Rules Analyzed: ").append(decisions.length).append("

\n"); + html.append("

Total Parser Time: ").append(String.format("%.2f ms (%,d ns)", totalTime / 1_000_000.0, totalTime)).append("

\n"); + html.append("

Analysis Date: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("

\n"); + html.append("
\n"); + + // Timing breakdown section + if (timingData != null) { + html.append("
\n"); + html.append("

Processing Time Breakdown

\n"); + html.append("

EFX Preprocessing: ").append(timingData.getPreprocessingTimeMs()).append(" ms (").append(String.format("%.1f%%", timingData.getPreprocessingPercentage())).append(")

\n"); + html.append("

EFX Translation: ").append(timingData.getTranslationTimeMs()).append(" ms (").append(String.format("%.1f%%", timingData.getTranslationPercentage())).append(")

\n"); + html.append("

Total Processing Time: ").append(timingData.getTotalTimeMs()).append(" ms

\n"); + html.append("
\n"); + } + + // Top performing grammar rules table + html.append("

Top 15 Most Time-Consuming Grammar Rules

\n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + + int displayCount = Math.min(15, decisions.length); + for (int i = 0; i < displayCount; i++) { + DecisionInfo decision = decisions[i]; + if (decision.timeInPrediction <= 0) + continue; + + String ruleName = (parser != null && parser.getRuleNames().length > decision.decision) + ? parser.getRuleNames()[decision.decision] + : "Unknown Rule #" + decision.decision; + + double timeMs = decision.timeInPrediction / 1_000_000.0; + double percentage = (double) decision.timeInPrediction / totalTime * 100; + double avgTimePerInvocation = decision.invocations > 0 + ? decision.timeInPrediction / (1000.0 * decision.invocations) + : 0; + + // Apply CSS class based on time percentage + String rowClass = ""; + if (percentage > 10) { + rowClass = "high-time"; + } else if (percentage > 5) { + rowClass = "medium-time"; + } + + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + html.append(" \n"); + } + + html.append(" \n"); + html.append("
RankGrammar RuleTime (ms)Time (ns)% of TotalInvocationsAmbiguitiesErrorsAvg Time/Invocation (μs)
").append(i + 1).append("").append(escapeHtml(ruleName)).append("").append(String.format("%.2f", timeMs)).append("").append(String.format("%,d", decision.timeInPrediction)).append("").append(String.format("%.1f%%", percentage)).append("").append(String.format("%,d", decision.invocations)).append("").append(decision.ambiguities.size()).append("").append(decision.errors.size()).append("").append(String.format("%.2f", avgTimePerInvocation)).append("
\n"); + + // Footer + html.append("

Generated by EFX Toolkit Profiler

\n"); + html.append("\n"); + html.append("\n"); + + return html.toString(); + } + + /** + * Escapes HTML special characters in the given text. + * + * @param text The text to escape + * @return HTML-escaped text + */ + private static String escapeHtml(String text) { + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java b/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java new file mode 100644 index 0000000..a266a94 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java @@ -0,0 +1,60 @@ +package eu.europa.ted.efx.util; + +/** + * Holds timing measurements for EFX template processing phases. + * This data is used to generate comprehensive performance reports. + */ +public class TranslatorTimings { + + private final long preprocessingTimeMs; + private final long translationTimeMs; + private final long totalTimeMs; + + /** + * Creates a new TimingData instance with the specified timing measurements. + * + * @param preprocessingTimeMs Time spent in EFX preprocessing phase (milliseconds) + * @param translationTimeMs Time spent in EFX translation phase (milliseconds) + * @param totalTimeMs Total EFX processing time (milliseconds) + */ + public TranslatorTimings(long preprocessingTimeMs, long translationTimeMs, long totalTimeMs) { + this.preprocessingTimeMs = preprocessingTimeMs; + this.translationTimeMs = translationTimeMs; + this.totalTimeMs = totalTimeMs; + } + + /** + * @return Time spent in EFX preprocessing phase (milliseconds) + */ + public long getPreprocessingTimeMs() { + return preprocessingTimeMs; + } + + /** + * @return Time spent in EFX translation phase (milliseconds) + */ + public long getTranslationTimeMs() { + return translationTimeMs; + } + + /** + * @return Total EFX processing time (milliseconds) + */ + public long getTotalTimeMs() { + return totalTimeMs; + } + + /** + * @return Percentage of total time spent in preprocessing + */ + public double getPreprocessingPercentage() { + return totalTimeMs > 0 ? (double) preprocessingTimeMs / totalTimeMs * 100.0 : 0.0; + } + + /** + * @return Percentage of total time spent in translation + */ + public double getTranslationPercentage() { + return totalTimeMs > 0 ? (double) translationTimeMs / totalTimeMs * 100.0 : 0.0; + } +} \ No newline at end of file From 01c18c65a0a6585e69d1566fd24386bc55413c8c Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Tue, 5 Aug 2025 02:06:23 +0200 Subject: [PATCH 4/4] Fix null handling in escapeHtml method and correct constructor documentation in TranslatorTimings --- .../eu/europa/ted/efx/util/EfxProfilerReportGenerator.java | 3 +++ src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java b/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java index 47c3e49..d72e866 100644 --- a/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java +++ b/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java @@ -182,6 +182,9 @@ public static String generateProfilerReport(final EfxParser parser, final Decisi * @return HTML-escaped text */ private static String escapeHtml(String text) { + if (text == null) { + return ""; + } return text.replace("&", "&") .replace("<", "<") .replace(">", ">") diff --git a/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java b/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java index a266a94..980f198 100644 --- a/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java +++ b/src/main/java/eu/europa/ted/efx/util/TranslatorTimings.java @@ -11,7 +11,7 @@ public class TranslatorTimings { private final long totalTimeMs; /** - * Creates a new TimingData instance with the specified timing measurements. + * Creates a new TranslatorTimings instance with the specified timing measurements. * * @param preprocessingTimeMs Time spent in EFX preprocessing phase (milliseconds) * @param translationTimeMs Time spent in EFX translation phase (milliseconds)