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/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 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 @@ -359,6 +415,7 @@ public void enterTemplateDefinition(TemplateDefinitionContext ctx) { @Override public void enterTemplateFile(TemplateFileContext ctx) { assert blockStack.isEmpty() : UNEXPECTED_INDENTATION; + this.translatorContext.setCurrentSection(TemplateSection.DEFAULT); } @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..d72e866 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/util/EfxProfilerReportGenerator.java @@ -0,0 +1,193 @@ +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) { + if (text == null) { + return ""; + } + 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..980f198 --- /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 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) + * @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