diff --git a/exist-core/src/main/java/org/exist/xquery/GeneralComparison.java b/exist-core/src/main/java/org/exist/xquery/GeneralComparison.java index 0a79c22c733..35fb02140a7 100644 --- a/exist-core/src/main/java/org/exist/xquery/GeneralComparison.java +++ b/exist-core/src/main/java/org/exist/xquery/GeneralComparison.java @@ -240,6 +240,17 @@ public void visitCastExpr( CastExpression expression ) } } } + + // Log optimization decisions + if (LOG.isDebugEnabled()) { + if (optimizeSelf || optimizeChild) { + LOG.debug("Optimizer: {} can use index optimization on {} (self={}, child={}, qname={})", + ExpressionDumper.dump(this), contextQName, optimizeSelf, optimizeChild, contextQName); + } else if (!steps.isEmpty()) { + LOG.debug("Optimizer: {} skipped index optimization — no suitable index path found", + ExpressionDumper.dump(this)); + } + } } @Override diff --git a/exist-core/src/main/java/org/exist/xquery/Profiler.java b/exist-core/src/main/java/org/exist/xquery/Profiler.java index aab76e1a050..95767629082 100644 --- a/exist-core/src/main/java/org/exist/xquery/Profiler.java +++ b/exist-core/src/main/java/org/exist/xquery/Profiler.java @@ -81,6 +81,17 @@ public class Profiler { private PerformanceStats stats; + /** + * Returns the performance statistics collected by this profiler instance. + * Each XQueryContext has its own Profiler with its own stats, enabling + * per-query profiling isolation. + * + * @return the performance stats for this profiler + */ + public PerformanceStats getPerformanceStats() { + return stats; + } + private long queryStart = 0; private Database db; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunExplain.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunExplain.java new file mode 100644 index 00000000000..0f4ebe014fb --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunExplain.java @@ -0,0 +1,150 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.exist.dom.QName; +import org.exist.dom.memtree.DocumentImpl; +import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.xquery.*; +import org.exist.xquery.parser.XQueryLexer; +import org.exist.xquery.parser.XQueryParser; +import org.exist.xquery.parser.XQueryTreeParser; +import org.exist.xquery.value.*; + +import antlr.collections.AST; + +import java.io.StringReader; + +/** + * Returns the compiled expression tree of an XQuery expression as XML. + * This is the core query visibility function — shows what the optimizer produces. + * + *
+ * util:explain('for $x in 1 to 10 where $x > 5 return $x * 2')
+ * 
+ * + * Returns an XML representation of the expression tree showing FLWOR clauses, + * path expressions, function calls, comparisons, etc. + */ +public class FunExplain extends BasicFunction { + + private static final Logger LOG = LogManager.getLogger(FunExplain.class); + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("explain", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Compiles the given XQuery expression and returns its expression tree as XML. " + + "Shows the post-optimization query plan.", + new SequenceType[]{ + new FunctionParameterSequenceType("query", Type.STRING, Cardinality.EXACTLY_ONE, + "The XQuery expression to explain") + }, + new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE, + "An XML representation of the compiled expression tree") + ), + new FunctionSignature( + new QName("explain", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Compiles the given XQuery expression and returns its expression tree as XML. " + + "The module-load-path controls where imports are resolved.", + new SequenceType[]{ + new FunctionParameterSequenceType("query", Type.STRING, Cardinality.EXACTLY_ONE, + "The XQuery expression to explain"), + new FunctionParameterSequenceType("module-load-path", Type.STRING, Cardinality.EXACTLY_ONE, + "The module load path for resolving imports") + }, + new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE, + "An XML representation of the compiled expression tree") + ) + }; + + public FunExplain(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final String query = args[0].getStringValue(); + if (query.trim().isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, "Query expression is empty"); + } + + // Compile the query using the same pattern as util:compile() + final XQueryContext pContext = new XQueryContext(context.getBroker().getBrokerPool()); + context.pushNamespaceContext(); + try { + if (getArgumentCount() == 2 && args[1].hasOne()) { + pContext.setModuleLoadPath(args[1].getStringValue()); + } + + final XQueryLexer lexer = new XQueryLexer(pContext, new StringReader(query)); + final XQueryParser parser = new XQueryParser(lexer); + final XQueryTreeParser astParser = new XQueryTreeParser(pContext); + + parser.xpath(); + if (parser.foundErrors()) { + throw new XPathException(this, ErrorCodes.XPST0003, + "Parse error in query: " + parser.getErrorMessage()); + } + + final AST ast = parser.getAST(); + final PathExpr path = new PathExpr(pContext); + astParser.xpath(ast, path); + if (astParser.foundErrors()) { + throw astParser.getLastException(); + } + + // Analyze (optimize) the expression tree + path.analyze(new AnalyzeContextInfo()); + + // Serialize the expression tree as XML + return serializeExpressionTree(path); + + } catch (final Exception e) { + throw new XPathException(this, ErrorCodes.XPST0003, "Parse error: " + e.getMessage()); + } finally { + context.popNamespaceContext(); + pContext.reset(false); + } + } + + private Sequence serializeExpressionTree(final Expression expression) throws XPathException { + context.pushDocumentContext(); + try { + final MemTreeBuilder builder = context.getDocumentBuilder(); + + builder.startElement("", "explain", "explain", null); + + final QueryPlanSerializer visitor = new QueryPlanSerializer(builder); + expression.accept(visitor); + + builder.endElement(); + + final DocumentImpl doc = (DocumentImpl) builder.getDocument(); + // Return the root element, not the document node + return (org.exist.dom.memtree.ElementImpl) doc.getDocumentElement(); + } finally { + context.popDocumentContext(); + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunIndexReport.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunIndexReport.java new file mode 100644 index 00000000000..0645d340689 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunIndexReport.java @@ -0,0 +1,142 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.exist.dom.QName; +import org.exist.dom.memtree.DocumentImpl; +import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.xquery.*; +import org.exist.xquery.parser.XQueryLexer; +import org.exist.xquery.parser.XQueryParser; +import org.exist.xquery.parser.XQueryTreeParser; +import org.exist.xquery.value.*; + +import antlr.collections.AST; + +import java.io.StringReader; + +/** + * Execute a query with profiling and report which indexes were used. + * Returns an XML report showing index type, usage count, and elapsed time. + * + *
+ * util:index-report('collection("/db/data")//book[@year > 2020]')
+ * 
+ * + * Returns: + *
+ * <index-report xmlns="http://exist-db.org/xquery/profiling">
+ *   <index type="range" source="..." elapsed="0.5" calls="42" optimization-level="BASIC"/>
+ *   <optimization type="RANGE_IDX" source="..." line="1" column="15"/>
+ * </index-report>
+ * 
+ */ +public class FunIndexReport extends BasicFunction { + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("index-report", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Executes the given query with profiling enabled and returns an XML report " + + "showing which indexes were used and which optimizations were applied.", + new SequenceType[]{ + new FunctionParameterSequenceType("query", Type.STRING, Cardinality.EXACTLY_ONE, + "The XQuery expression to analyze for index usage") + }, + new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.EXACTLY_ONE, + "An XML report of index usage and optimizations") + ) + }; + + public FunIndexReport(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final String query = args[0].getStringValue(); + if (query.trim().isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, "Query expression is empty"); + } + + final XQueryContext pContext = new XQueryContext(context.getBroker().getBrokerPool()); + context.pushNamespaceContext(); + + try { + // Enable profiling with full verbosity + final Profiler profiler = pContext.getProfiler(); + profiler.configure(new Option( + this, + new QName("profiling", "http://exist-db.org/xquery/util", "exist"), + "enabled=yes verbosity=10" + )); + + // Compile + final XQueryLexer lexer = new XQueryLexer(pContext, new StringReader(query)); + final XQueryParser parser = new XQueryParser(lexer); + final XQueryTreeParser astParser = new XQueryTreeParser(pContext); + + parser.xpath(); + if (parser.foundErrors()) { + throw new XPathException(this, ErrorCodes.XPST0003, + "Parse error in query: " + parser.getErrorMessage()); + } + + final AST ast = parser.getAST(); + final PathExpr path = new PathExpr(pContext); + astParser.xpath(ast, path); + if (astParser.foundErrors()) { + throw astParser.getLastException(); + } + + path.analyze(new AnalyzeContextInfo()); + + // Execute to trigger index usage + path.eval(null, null); + + // Serialize the per-query profiler stats as the index report + final PerformanceStats stats = profiler.getPerformanceStats(); + context.pushDocumentContext(); + try { + final MemTreeBuilder builder = context.getDocumentBuilder(); + if (stats instanceof PerformanceStatsImpl) { + ((PerformanceStatsImpl) stats).serialize(builder); + } else { + builder.startElement("", "index-report", "index-report", null); + builder.endElement(); + } + final DocumentImpl doc = (DocumentImpl) builder.getDocument(); + return (org.exist.dom.memtree.ElementImpl) doc.getDocumentElement(); + } finally { + context.popDocumentContext(); + } + + } catch (final Exception e) { + if (e instanceof XPathException) { + throw (XPathException) e; + } + throw new XPathException(this, ErrorCodes.FOER0000, "Error profiling query: " + e.getMessage()); + } finally { + context.popNamespaceContext(); + pContext.reset(false); + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunMemory.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunMemory.java new file mode 100644 index 00000000000..71b7503bd2d --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunMemory.java @@ -0,0 +1,106 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.exist.dom.QName; +import org.exist.xquery.*; +import org.exist.xquery.value.*; + +/** + * Pass-through profiling function that measures memory usage of an expression. + * Returns the expression result unchanged, logging the memory delta. + * + *

Inspired by BaseX's prof:memory().

+ * + *
+ * util:memory(parse-json(unparsed-text("/db/large.json")))
+ * util:memory(parse-json(unparsed-text("/db/large.json")), "JSON parse")
+ * 
+ */ +public class FunMemory extends BasicFunction { + + private static final Logger LOG = LogManager.getLogger(FunMemory.class); + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("memory", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures the memory usage of the given expression and logs it. Returns the result unchanged.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure") + }, + new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, + "The result of the expression, unchanged") + ), + new FunctionSignature( + new QName("memory", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures the memory usage of the given expression and logs it with a label. Returns the result unchanged.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure"), + new FunctionParameterSequenceType("label", Type.STRING, Cardinality.EXACTLY_ONE, + "A label for the log message") + }, + new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, + "The result of the expression, unchanged") + ) + }; + + public FunMemory(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final Runtime runtime = Runtime.getRuntime(); + final long memBefore = runtime.totalMemory() - runtime.freeMemory(); + + // The expression has already been evaluated — args[0] contains the result + final Sequence result = args[0]; + + final long memAfter = runtime.totalMemory() - runtime.freeMemory(); + final long memDelta = memAfter - memBefore; + + final String label = getArgumentCount() == 2 + ? args[1].getStringValue() + : "util:memory()"; + + LOG.info("{} \u2014 {}", label, formatBytes(memDelta)); + + return result; + } + + static String formatBytes(final long bytes) { + final long abs = Math.abs(bytes); + if (abs < 1024) { + return bytes + " B"; + } else if (abs < 1024 * 1024) { + return String.format("%.1f KB", bytes / 1024.0); + } else if (abs < 1024 * 1024 * 1024) { + return String.format("%.1f MB", bytes / (1024.0 * 1024)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunProfile.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunProfile.java new file mode 100644 index 00000000000..cdab46248d2 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunProfile.java @@ -0,0 +1,223 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.exist.dom.QName; +import org.exist.dom.memtree.DocumentImpl; +import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.source.StringSource; +import org.exist.storage.DBBroker; +import org.exist.xquery.*; +import org.exist.xquery.functions.map.MapType; +import org.exist.xquery.parser.XQueryLexer; +import org.exist.xquery.parser.XQueryParser; +import org.exist.xquery.parser.XQueryTreeParser; +import org.exist.xquery.value.*; + +import antlr.collections.AST; + +import java.io.StringReader; + +/** + * Execute a query with profiling enabled and return structured profiling data. + * Combines timing, memory measurement, expression tree, and index/optimization + * statistics into a single map result. + * + *
+ * let $p := util:profile('collection("/db/data")//book[year > 2020]')
+ * return (
+ *   $p?result,          (: the query result :)
+ *   $p?time,            (: xs:dayTimeDuration :)
+ *   $p?memory,          (: xs:integer bytes :)
+ *   $p?plan,            (: element(explain) — expression tree :)
+ *   $p?stats            (: element() — profiler statistics XML :)
+ * )
+ * 
+ */ +public class FunProfile extends BasicFunction { + + private static final Logger LOG = LogManager.getLogger(FunProfile.class); + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("profile", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Executes the given query with profiling enabled and returns a map with " + + "the result, timing, memory, expression tree, and profiler statistics.", + new SequenceType[]{ + new FunctionParameterSequenceType("query", Type.STRING, Cardinality.EXACTLY_ONE, + "The XQuery expression to profile") + }, + new FunctionReturnSequenceType(Type.MAP_ITEM, Cardinality.EXACTLY_ONE, + "A map with keys: result, time, memory, plan, stats") + ), + new FunctionSignature( + new QName("profile", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Executes the given query with profiling enabled and returns a map with " + + "the result, timing, memory, expression tree, and profiler statistics.", + new SequenceType[]{ + new FunctionParameterSequenceType("query", Type.STRING, Cardinality.EXACTLY_ONE, + "The XQuery expression to profile"), + new FunctionParameterSequenceType("module-load-path", Type.STRING, Cardinality.EXACTLY_ONE, + "The module load path for resolving imports") + }, + new FunctionReturnSequenceType(Type.MAP_ITEM, Cardinality.EXACTLY_ONE, + "A map with keys: result, time, memory, plan, stats") + ) + }; + + public FunProfile(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final String query = args[0].getStringValue(); + if (query.trim().isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, "Query expression is empty"); + } + + final DBBroker broker = context.getBroker(); + final XQueryContext pContext = new XQueryContext(broker.getBrokerPool()); + context.pushNamespaceContext(); + + try { + if (getArgumentCount() == 2 && args[1].hasOne()) { + pContext.setModuleLoadPath(args[1].getStringValue()); + } + + // Enable profiling on the context + final Profiler profiler = pContext.getProfiler(); + profiler.configure(new Option( + this, + new QName("profiling", "http://exist-db.org/xquery/util", "exist"), + "enabled=yes verbosity=10" + )); + + // Compile the query + final XQueryLexer lexer = new XQueryLexer(pContext, new StringReader(query)); + final XQueryParser parser = new XQueryParser(lexer); + final XQueryTreeParser astParser = new XQueryTreeParser(pContext); + + parser.xpath(); + if (parser.foundErrors()) { + throw new XPathException(this, ErrorCodes.XPST0003, + "Parse error in query: " + parser.getErrorMessage()); + } + + final AST ast = parser.getAST(); + final PathExpr path = new PathExpr(pContext); + astParser.xpath(ast, path); + if (astParser.foundErrors()) { + throw astParser.getLastException(); + } + + // Analyze (optimize) + path.analyze(new AnalyzeContextInfo()); + + // Generate the expression plan BEFORE execution + final Sequence plan = serializeExpressionTree(path); + + // Execute with timing and memory measurement + final Runtime runtime = Runtime.getRuntime(); + final long memBefore = runtime.totalMemory() - runtime.freeMemory(); + final long startNanos = System.nanoTime(); + + final Sequence result = path.eval(null, null); + + final long elapsedNanos = System.nanoTime() - startNanos; + final long memAfter = runtime.totalMemory() - runtime.freeMemory(); + final long memDelta = memAfter - memBefore; + + // Capture profiler stats as XML + final Sequence statsXml = serializeProfilerStats(pContext); + + // Build the result map + final MapType resultMap = new MapType(this, context); + + resultMap.add(new StringValue("result"), result); + + try { + resultMap.add(new StringValue("time"), + new DayTimeDurationValue(this, elapsedNanos / 1_000_000)); + } catch (final XPathException e) { + resultMap.add(new StringValue("time"), + new IntegerValue(this, elapsedNanos / 1_000_000)); + } + + resultMap.add(new StringValue("memory"), new IntegerValue(this, memDelta)); + resultMap.add(new StringValue("plan"), plan); + resultMap.add(new StringValue("stats"), statsXml); + + return resultMap; + + } catch (final Exception e) { + if (e instanceof XPathException) { + throw (XPathException) e; + } + throw new XPathException(this, ErrorCodes.XPST0003, "Error profiling query: " + e.getMessage()); + } finally { + context.popNamespaceContext(); + pContext.reset(false); + } + } + + private Sequence serializeExpressionTree(final Expression expression) throws XPathException { + context.pushDocumentContext(); + try { + final MemTreeBuilder builder = context.getDocumentBuilder(); + builder.startElement("", "explain", "explain", null); + final QueryPlanSerializer visitor = new QueryPlanSerializer(builder); + expression.accept(visitor); + builder.endElement(); + final DocumentImpl doc = (DocumentImpl) builder.getDocument(); + return (org.exist.dom.memtree.ElementImpl) doc.getDocumentElement(); + } finally { + context.popDocumentContext(); + } + } + + /** + * Serialize the per-query profiler's performance stats as XML. + * Uses the profiled query's own Profiler instance (not the global + * BrokerPool stats) to ensure per-query isolation under concurrent load. + */ + private Sequence serializeProfilerStats(final XQueryContext pContext) throws XPathException { + // Read from the per-query profiler's stats, not the global BrokerPool stats + final PerformanceStats stats = pContext.getProfiler().getPerformanceStats(); + context.pushDocumentContext(); + try { + final MemTreeBuilder builder = context.getDocumentBuilder(); + if (stats instanceof PerformanceStatsImpl) { + ((PerformanceStatsImpl) stats).serialize(builder); + } else { + builder.startElement("", "stats", "stats", null); + builder.endElement(); + } + final DocumentImpl doc = (DocumentImpl) builder.getDocument(); + return (org.exist.dom.memtree.ElementImpl) doc.getDocumentElement(); + } finally { + context.popDocumentContext(); + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunTime.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunTime.java new file mode 100644 index 00000000000..d678581c3c5 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunTime.java @@ -0,0 +1,103 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.exist.dom.QName; +import org.exist.xquery.*; +import org.exist.xquery.value.*; + +/** + * Pass-through profiling function that measures execution time of an expression. + * Returns the expression result unchanged, logging the elapsed time. + * + *

Inspired by BaseX's prof:time().

+ * + *
+ * util:time(collection("/db/data")//title)
+ * util:time(collection("/db/data")//title, "title lookup")
+ * 
+ */ +public class FunTime extends BasicFunction { + + private static final Logger LOG = LogManager.getLogger(FunTime.class); + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("time", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures the execution time of the given expression and logs it. Returns the result unchanged.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure") + }, + new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, + "The result of the expression, unchanged") + ), + new FunctionSignature( + new QName("time", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures the execution time of the given expression and logs it with a label. Returns the result unchanged.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure"), + new FunctionParameterSequenceType("label", Type.STRING, Cardinality.EXACTLY_ONE, + "A label for the log message") + }, + new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, + "The result of the expression, unchanged") + ) + }; + + public FunTime(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final long startNanos = System.nanoTime(); + + // The expression has already been evaluated by the function call mechanism — + // args[0] contains the result + final Sequence result = args[0]; + + final long elapsedNanos = System.nanoTime() - startNanos; + final double elapsedMs = elapsedNanos / 1_000_000.0; + + final String label = getArgumentCount() == 2 + ? args[1].getStringValue() + : "util:time()"; + + LOG.info("{} \u2014 {}", label, formatDuration(elapsedMs)); + + return result; + } + + static String formatDuration(final double ms) { + if (ms < 1.0) { + return String.format("%.1f\u00B5s", ms * 1000); + } else if (ms < 1000.0) { + return String.format("%.1fms", ms); + } else { + return String.format("%.2fs", ms / 1000); + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/FunTrack.java b/exist-core/src/main/java/org/exist/xquery/functions/util/FunTrack.java new file mode 100644 index 00000000000..864c815f0f9 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/FunTrack.java @@ -0,0 +1,115 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.exist.dom.QName; +import org.exist.xquery.*; +import org.exist.xquery.functions.map.MapType; +import org.exist.xquery.value.*; + + +/** + * Profiling function that measures execution time and memory, returning + * a map with the measurements and the expression result. + * + *

Inspired by BaseX's prof:track().

+ * + *
+ * let $r := util:track(collection("/db/data")//title)
+ * return (
+ *   "Time: " || $r?time,
+ *   "Memory: " || $r?memory || " bytes",
+ *   "Items: " || count($r?value)
+ * )
+ * 
+ */ +public class FunTrack extends BasicFunction { + + public static final FunctionSignature[] signatures = { + new FunctionSignature( + new QName("track", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures execution time and memory of the given expression. " + + "Returns map { 'time': xs:dayTimeDuration, 'memory': xs:integer, 'value': item()* }.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure") + }, + new FunctionReturnSequenceType(Type.MAP_ITEM, Cardinality.EXACTLY_ONE, + "A map with keys 'time' (xs:dayTimeDuration), 'memory' (xs:integer bytes), 'value' (the result)") + ), + new FunctionSignature( + new QName("track", UtilModule.NAMESPACE_URI, UtilModule.PREFIX), + "Measures execution time and memory of the given expression with a label. " + + "Returns map { 'time': xs:dayTimeDuration, 'memory': xs:integer, 'value': item()*, 'label': xs:string }.", + new SequenceType[]{ + new FunctionParameterSequenceType("expr", Type.ITEM, Cardinality.ZERO_OR_MORE, + "The expression to measure"), + new FunctionParameterSequenceType("label", Type.STRING, Cardinality.EXACTLY_ONE, + "A label for identifying this measurement") + }, + new FunctionReturnSequenceType(Type.MAP_ITEM, Cardinality.EXACTLY_ONE, + "A map with keys 'time', 'memory', 'value', and 'label'") + ) + }; + + public FunTrack(final XQueryContext context, final FunctionSignature signature) { + super(context, signature); + } + + @Override + public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { + final Runtime runtime = Runtime.getRuntime(); + final long memBefore = runtime.totalMemory() - runtime.freeMemory(); + final long startNanos = System.nanoTime(); + + // The expression has already been evaluated — args[0] contains the result + final Sequence result = args[0]; + + final long elapsedNanos = System.nanoTime() - startNanos; + final long memAfter = runtime.totalMemory() - runtime.freeMemory(); + final long memDelta = memAfter - memBefore; + + // Build the result map + final MapType map = new MapType(this, context); + + // time as xs:dayTimeDuration (millisecond precision) + try { + map.add(new StringValue("time"), new DayTimeDurationValue(this, elapsedNanos / 1_000_000)); + } catch (final XPathException e) { + // Fallback: store milliseconds as integer + map.add(new StringValue("time"), new IntegerValue(this, elapsedNanos / 1_000_000)); + } + + // memory as xs:integer (bytes) + map.add(new StringValue("memory"), new IntegerValue(this, memDelta)); + + // value: the expression result + map.add(new StringValue("value"), result); + + // optional label + if (getArgumentCount() == 2) { + map.add(new StringValue("label"), new StringValue(args[1].getStringValue())); + } + + return map; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/QueryPlanSerializer.java b/exist-core/src/main/java/org/exist/xquery/functions/util/QueryPlanSerializer.java new file mode 100644 index 00000000000..07d25354c9e --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/QueryPlanSerializer.java @@ -0,0 +1,389 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.functions.util; + +import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.xquery.*; +import org.exist.xquery.value.AtomicValue; +import org.exist.xquery.value.Sequence; + +import javax.annotation.Nullable; + +/** + * Serializes a compiled XQuery expression tree as XML. + * Extends DefaultExpressionVisitor to walk the tree and emit XML elements + * for each expression type. + * + *

Output namespace: http://exist-db.org/xquery/profiling

+ */ +public class QueryPlanSerializer extends DefaultExpressionVisitor { + + public static final String NAMESPACE = "http://exist-db.org/xquery/profiling"; + + private final MemTreeBuilder builder; + + public QueryPlanSerializer(final MemTreeBuilder builder) { + this.builder = builder; + } + + // === Helper methods === + + private void startElement(final String name) { + builder.startElement("", name, name, null); + } + + private void startElement(final String name, final String[][] attrs) { + final org.xml.sax.helpers.AttributesImpl atts = new org.xml.sax.helpers.AttributesImpl(); + for (final String[] attr : attrs) { + if (attr[1] != null) { + atts.addAttribute("", attr[0], attr[0], "CDATA", attr[1]); + } + } + builder.startElement("", name, name, atts); + } + + private void endElement() { + builder.endElement(); + } + + private void textElement(final String name, final String text) { + startElement(name); + builder.characters(text); + endElement(); + } + + private void addLocation(final Expression expr) { + if (expr.getLine() > 0) { + // Line/column info is on the element already started by the caller + } + } + + private String[][] locationAttrs(final Expression expr) { + if (expr.getLine() > 0) { + return new String[][]{ + {"line", String.valueOf(expr.getLine())}, + {"column", String.valueOf(expr.getColumn())} + }; + } + return new String[0][]; + } + + private String[][] mergeAttrs(final String[][]... attrSets) { + int total = 0; + for (final String[][] set : attrSets) total += set.length; + final String[][] result = new String[total][]; + int idx = 0; + for (final String[][] set : attrSets) { + System.arraycopy(set, 0, result, idx, set.length); + idx += set.length; + } + return result; + } + + // === Visitor methods === + + @Override + public void visit(final Expression expression) { + final String className = expression.getClass().getSimpleName(); + if (className.isEmpty()) { + startElement("expression", mergeAttrs( + new String[][]{{"class", expression.getClass().getName()}}, + locationAttrs(expression))); + } else { + startElement("expression", mergeAttrs( + new String[][]{{"type", className}}, + locationAttrs(expression))); + } + endElement(); + } + + @Override + public void visitPathExpr(final PathExpr expression) { + if (expression.getLength() == 1) { + // Unwrap single-step path expressions + expression.getExpression(0).accept(this); + return; + } + startElement("path", locationAttrs(expression)); + for (int i = 0; i < expression.getLength(); i++) { + expression.getExpression(i).accept(this); + } + endElement(); + } + + @Override + public void visitLocationStep(final LocationStep locationStep) { + startElement("step", mergeAttrs( + new String[][]{ + {"axis", org.exist.xquery.Constants.AXISSPECIFIERS[locationStep.getAxis()]}, + {"test", locationStep.getTest().toString()} + }, + locationAttrs(locationStep))); + @Nullable final Predicate[] predicates = locationStep.getPredicates(); + if (predicates != null) { + for (final Predicate pred : predicates) { + pred.accept(this); + } + } + endElement(); + } + + @Override + public void visitFilteredExpr(final FilteredExpression filtered) { + startElement("filter", locationAttrs(filtered)); + filtered.getExpression().accept(this); + for (final Predicate pred : filtered.getPredicates()) { + pred.accept(this); + } + endElement(); + } + + @Override + public void visitPredicate(final Predicate predicate) { + startElement("predicate", locationAttrs(predicate)); + predicate.getExpression(0).accept(this); + endElement(); + } + + @Override + public void visitFunctionCall(final FunctionCall call) { + final String name = call.getFunction().getName().getStringValue(); + startElement("function-call", mergeAttrs( + new String[][]{ + {"name", name}, + {"arity", String.valueOf(call.getArgumentCount())} + }, + locationAttrs(call))); + for (int i = 0; i < call.getArgumentCount(); i++) { + call.getArgument(i).accept(this); + } + endElement(); + } + + @Override + public void visitBuiltinFunction(final Function function) { + startElement("builtin-function", mergeAttrs( + new String[][]{ + {"name", function.getName().getStringValue()}, + {"arity", String.valueOf(function.getArgumentCount())} + }, + locationAttrs(function))); + for (int i = 0; i < function.getArgumentCount(); i++) { + function.getArgument(i).accept(this); + } + endElement(); + } + + @Override + public void visitUserFunction(final UserDefinedFunction function) { + startElement("user-function", new String[][]{ + {"name", function.getName().getStringValue()}, + {"arity", String.valueOf(function.getSignature().getArgumentCount())} + }); + function.getFunctionBody().accept(this); + endElement(); + } + + @Override + public void visitForExpression(final ForExpr forExpr) { + startElement("for", mergeAttrs( + new String[][]{{"variable", "$" + forExpr.getVariable().getStringValue()}}, + locationAttrs(forExpr))); + startElement("in"); + forExpr.getInputSequence().accept(this); + endElement(); + forExpr.getReturnExpression().accept(this); + endElement(); + } + + @Override + public void visitLetExpression(final LetExpr letExpr) { + startElement("let", mergeAttrs( + new String[][]{{"variable", "$" + letExpr.getVariable().getStringValue()}}, + locationAttrs(letExpr))); + startElement("value"); + letExpr.getInputSequence().accept(this); + endElement(); + letExpr.getReturnExpression().accept(this); + endElement(); + } + + @Override + public void visitWhereClause(final WhereClause where) { + startElement("where", locationAttrs(where)); + where.getWhereExpr().accept(this); + endElement(); + where.getReturnExpression().accept(this); + } + + @Override + public void visitOrderByClause(final OrderByClause orderBy) { + startElement("order-by", locationAttrs(orderBy)); + for (final OrderSpec spec : orderBy.getOrderSpecs()) { + startElement("order-spec"); + spec.getSortExpression().accept(this); + endElement(); + } + endElement(); + orderBy.getReturnExpression().accept(this); + } + + @Override + public void visitGroupByClause(final GroupByClause groupBy) { + startElement("group-by", locationAttrs(groupBy)); + for (final GroupSpec spec : groupBy.getGroupSpecs()) { + startElement("group-spec"); + spec.getGroupExpression().accept(this); + endElement(); + } + endElement(); + groupBy.getReturnExpression().accept(this); + } + + @Override + public void visitGeneralComparison(final GeneralComparison comparison) { + startElement("comparison", mergeAttrs( + new String[][]{{"operator", comparison.toString().contains("=") ? "eq" : "cmp"}}, + locationAttrs(comparison))); + comparison.getLeft().accept(this); + comparison.getRight().accept(this); + endElement(); + } + + @Override + public void visitAndExpr(final OpAnd and) { + startElement("and", locationAttrs(and)); + and.getLeft().accept(this); + and.getRight().accept(this); + endElement(); + } + + @Override + public void visitOrExpr(final OpOr or) { + startElement("or", locationAttrs(or)); + or.getLeft().accept(this); + or.getRight().accept(this); + endElement(); + } + + @Override + public void visitConditional(final ConditionalExpression conditional) { + startElement("if", locationAttrs(conditional)); + startElement("test"); + conditional.getTestExpr().accept(this); + endElement(); + startElement("then"); + conditional.getThenExpr().accept(this); + endElement(); + startElement("else"); + conditional.getElseExpr().accept(this); + endElement(); + endElement(); + } + + @Override + public void visitCastExpr(final CastExpression expression) { + startElement("cast", mergeAttrs( + new String[][]{{"cardinality", expression.getCardinality().getHumanDescription()}}, + locationAttrs(expression))); + endElement(); + } + + @Override + public void visitUnionExpr(final Union union) { + startElement("union", locationAttrs(union)); + union.getLeft().accept(this); + union.getRight().accept(this); + endElement(); + } + + @Override + public void visitIntersectionExpr(final Intersect intersect) { + startElement("intersect", locationAttrs(intersect)); + intersect.getLeft().accept(this); + intersect.getRight().accept(this); + endElement(); + } + + @Override + public void visitVariableReference(final VariableReference ref) { + startElement("variable", mergeAttrs( + new String[][]{{"name", "$" + ref.getName().toString()}}, + locationAttrs(ref))); + endElement(); + } + + @Override + public void visitVariableDeclaration(final VariableDeclaration decl) { + startElement("variable-declaration", mergeAttrs( + new String[][]{{"name", "$" + decl.getName().toString()}}, + locationAttrs(decl))); + decl.getExpression().ifPresent(e -> e.accept(this)); + endElement(); + } + + @Override + public void visitDocumentConstructor(final DocumentConstructor constructor) { + startElement("document-constructor", locationAttrs(constructor)); + constructor.getContent().accept(this); + endElement(); + } + + @Override + public void visitElementConstructor(final ElementConstructor constructor) { + startElement("element-constructor", locationAttrs(constructor)); + constructor.getNameExpr().accept(this); + if (constructor.getContent() != null) { + constructor.getContent().accept(this); + } + endElement(); + } + + @Override + public void visitTextConstructor(final DynamicTextConstructor constructor) { + startElement("text-constructor", locationAttrs(constructor)); + constructor.getContent().accept(this); + endElement(); + } + + @Override + public void visitTryCatch(final TryCatchExpression tryCatch) { + startElement("try-catch", locationAttrs(tryCatch)); + startElement("try"); + tryCatch.getTryTargetExpr().accept(this); + endElement(); + for (final TryCatchExpression.CatchClause clause : tryCatch.getCatchClauses()) { + startElement("catch"); + clause.getCatchExpr().accept(this); + endElement(); + } + endElement(); + } + + @Override + public void visitSimpleMapOperator(final OpSimpleMap simpleMap) { + startElement("simple-map", locationAttrs(simpleMap)); + simpleMap.getLeft().accept(this); + simpleMap.getRight().accept(this); + endElement(); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java index b9e49a04b9b..98c19cd1cc4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/util/UtilModule.java @@ -152,7 +152,21 @@ public class UtilModule extends AbstractInternalModule { new FunctionDef(Base64Functions.signatures[3], Base64Functions.class), new FunctionDef(BaseConversionFunctions.FNS_INT_TO_OCTAL, BaseConversionFunctions.class), new FunctionDef(BaseConversionFunctions.FNS_OCTAL_TO_INT, BaseConversionFunctions.class), - new FunctionDef(LineNumber.signature, LineNumber.class) + new FunctionDef(LineNumber.signature, LineNumber.class), + + // --- Query Profiling Functions --- + new FunctionDef(FunTime.signatures[0], FunTime.class), + new FunctionDef(FunTime.signatures[1], FunTime.class), + new FunctionDef(FunMemory.signatures[0], FunMemory.class), + new FunctionDef(FunMemory.signatures[1], FunMemory.class), + new FunctionDef(FunTrack.signatures[0], FunTrack.class), + new FunctionDef(FunTrack.signatures[1], FunTrack.class), + new FunctionDef(FunExplain.signatures[0], FunExplain.class), + new FunctionDef(FunExplain.signatures[1], FunExplain.class), + new FunctionDef(FunProfile.signatures[0], FunProfile.class), + new FunctionDef(FunProfile.signatures[1], FunProfile.class), + new FunctionDef(FunIndexReport.signatures[0], FunIndexReport.class) + // --- End Query Profiling Functions --- }; static { diff --git a/exist-core/src/test/xquery/util/profiling.xql b/exist-core/src/test/xquery/util/profiling.xql new file mode 100644 index 00000000000..4f822d64770 --- /dev/null +++ b/exist-core/src/test/xquery/util/profiling.xql @@ -0,0 +1,247 @@ +xquery version "3.1"; + +(:~ + : Tests for util:time(), util:memory(), and util:track() profiling functions. + :) +module namespace prof = "http://exist-db.org/xquery/test/profiling"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; + +(: === util:time tests === :) + +declare + %test:assertTrue +function prof:time-returns-result() { + let $result := util:time(1 + 1) + return $result eq 2 +}; + +declare + %test:assertEquals(5) +function prof:time-sequence() { + count(util:time(1 to 5)) +}; + +declare + %test:assertEquals("hello") +function prof:time-with-label() { + util:time("hello", "string test") +}; + +declare + %test:assertTrue +function prof:time-empty-sequence() { + empty(util:time(())) +}; + +(: === util:memory tests === :) + +declare + %test:assertTrue +function prof:memory-returns-result() { + let $result := util:memory(1 + 1) + return $result eq 2 +}; + +declare + %test:assertEquals("world") +function prof:memory-with-label() { + util:memory("world", "memory test") +}; + +(: === util:track tests === :) + +declare + %test:assertTrue +function prof:track-returns-map() { + let $result := util:track(1 + 1) + return $result instance of map(*) +}; + +declare + %test:assertTrue +function prof:track-has-time-key() { + let $result := util:track(1 + 1) + return map:contains($result, "time") +}; + +declare + %test:assertTrue +function prof:track-has-memory-key() { + let $result := util:track(1 + 1) + return map:contains($result, "memory") +}; + +declare + %test:assertTrue +function prof:track-has-value-key() { + let $result := util:track(1 + 1) + return map:contains($result, "value") +}; + +declare + %test:assertEquals(2) +function prof:track-value-correct() { + let $result := util:track(1 + 1) + return $result?value +}; + +declare + %test:assertTrue +function prof:track-time-is-duration() { + let $result := util:track(1 to 100) + return $result?time instance of xs:dayTimeDuration +}; + +declare + %test:assertTrue +function prof:track-memory-is-integer() { + let $result := util:track(1 to 100) + return $result?memory instance of xs:integer +}; + +declare + %test:assertTrue +function prof:track-with-label() { + let $result := util:track(1 to 10, "range test") + return map:contains($result, "label") and $result?label eq "range test" +}; + +declare + %test:assertEquals(5) +function prof:track-sequence-value() { + let $result := util:track(1 to 5) + return count($result?value) +}; + +(: === util:explain tests === :) + +declare + %test:assertTrue +function prof:explain-returns-element() { + let $result := util:explain('1 + 1') + return $result instance of element() +}; + +declare + %test:assertEquals("explain") +function prof:explain-root-element() { + let $result := util:explain('1 + 1') + return local-name($result) +}; + +declare + %test:assertTrue +function prof:explain-for-has-for-element() { + let $result := util:explain('for $x in 1 to 5 return $x') + return exists($result//for) +}; + +declare + %test:assertTrue +function prof:explain-let-has-let-element() { + let $result := util:explain('let $x := 42 return $x') + return exists($result//let) +}; + +declare + %test:assertTrue +function prof:explain-path-has-step() { + let $result := util:explain('//title') + return exists($result//step) +}; + +declare + %test:assertTrue +function prof:explain-function-call() { + let $result := util:explain('count(1 to 10)') + return exists($result//*[contains(@name, "count")]) +}; + +declare + %test:assertTrue +function prof:explain-conditional() { + let $result := util:explain('if (true()) then 1 else 2') + return exists($result//if) +}; + +(: === util:profile tests === :) + +declare + %test:assertTrue +function prof:profile-returns-map() { + let $result := util:profile('1 + 1') + return $result instance of map(*) +}; + +declare + %test:assertTrue +function prof:profile-has-result-key() { + let $result := util:profile('1 + 1') + return map:contains($result, "result") +}; + +declare + %test:assertTrue +function prof:profile-has-time-key() { + let $result := util:profile('1 + 1') + return map:contains($result, "time") +}; + +declare + %test:assertTrue +function prof:profile-has-memory-key() { + let $result := util:profile('1 + 1') + return map:contains($result, "memory") +}; + +declare + %test:assertTrue +function prof:profile-has-plan-key() { + let $result := util:profile('1 + 1') + return map:contains($result, "plan") +}; + +declare + %test:assertTrue +function prof:profile-has-stats-key() { + let $result := util:profile('1 + 1') + return map:contains($result, "stats") +}; + +declare + %test:assertEquals(2) +function prof:profile-result-correct() { + let $result := util:profile('1 + 1') + return $result?result +}; + +declare + %test:assertTrue +function prof:profile-plan-is-element() { + let $result := util:profile('for $x in 1 to 5 return $x') + return $result?plan instance of element() +}; + +declare + %test:assertTrue +function prof:profile-stats-is-element() { + let $result := util:profile('1 + 1') + return $result?stats instance of element() +}; + +(: === util:index-report tests === :) + +declare + %test:assertTrue +function prof:index-report-returns-element() { + let $result := util:index-report('1 + 1') + return $result instance of element() +}; + +declare + %test:assertTrue +function prof:index-report-with-path() { + let $result := util:index-report('//title') + return $result instance of element() +};