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() +};