Skip to content

Add query profiling functions: util:time, memory, track, explain, profile, index-report#6194

Closed
joewiz wants to merge 4 commits intoeXist-db:developfrom
joewiz:feature/query-profiling
Closed

Add query profiling functions: util:time, memory, track, explain, profile, index-report#6194
joewiz wants to merge 4 commits intoeXist-db:developfrom
joewiz:feature/query-profiling

Conversation

@joewiz
Copy link
Copy Markdown
Member

@joewiz joewiz commented Mar 28, 2026

Summary

Add a suite of query profiling and visibility functions to eXist-db's util: module, inspired by BaseX's prof: module and addressing long-standing requests for query plan visibility (core dev feedback: "Make optimizations visible").

This PR adds 7 new functions across 3 categories: pass-through measurement, query plan inspection, and execution profiling with per-query isolation.

New Functions

Pass-Through Measurement

Function Returns Purpose
util:time(\$expr) item()* Measures and logs execution time; returns result unchanged
util:time(\$expr, \$label) item()* Same, with custom log label
util:memory(\$expr) item()* Measures and logs memory delta; returns result unchanged
util:memory(\$expr, \$label) item()* Same, with custom log label
util:track(\$expr) map(*) Returns map { "time": xs:dayTimeDuration, "memory": xs:integer, "value": item()* }
util:track(\$expr, \$label) map(*) Same, with label in the result map

Query Plan Inspection

Function Returns Purpose
util:explain(\$query) element(explain) Compiles query string and returns the post-optimization expression tree as XML
util:explain(\$query, \$module-load-path) element(explain) Same, with module import resolution path

Execution Profiling

Function Returns Purpose
util:profile(\$query) map(*) Executes query with profiling; returns map { "result", "time", "memory", "plan", "stats" }
util:profile(\$query, \$module-load-path) map(*) Same, with module import resolution path
util:index-report(\$query) element() Executes query and returns XML report of index usage and optimizations

What Changed

File Changes
FunTime.java Pass-through timer with formatted logging (µs/ms/s)
FunMemory.java Pass-through memory profiler with formatted logging (B/KB/MB/GB)
FunTrack.java Combined time + memory measurement returning structured map
FunExplain.java Compiles query, runs optimizer, serializes expression tree via QueryPlanSerializer
QueryPlanSerializer.java ExpressionVisitor that emits XML for ~20 expression types (FLWOR, path, comparison, function call, conditional, etc.)
FunProfile.java Combines execution, timing, memory, plan, and per-query profiler stats into a single map result
FunIndexReport.java Executes with profiling and returns PerformanceStats as XML report
Profiler.java Added getPerformanceStats() for per-query stats isolation
GeneralComparison.java Added DEBUG-level optimizer decision logging
UtilModule.java Registered all 11 new function signatures
profiling.xql 31 XQSuite tests covering all functions and signatures

Design Decisions

Per-query profiling isolation: Each util:profile() call creates its own XQueryContext with its own Profiler instance, which has its own PerformanceStatsImpl. This ensures profiling data is accurate under concurrent load — no crosstalk between queries.

Expression tree as XML: util:explain() uses a visitor pattern (QueryPlanSerializer extends DefaultExpressionVisitor) to walk the compiled expression tree and emit XML. This shows the post-optimization tree — what the engine actually executes.

Deferred: ValueComparison optimization (Phase 4.2). The profiling audit revealed that eq/ne/lt/gt (value comparisons) never trigger index optimizations — only =/!=/</> (general comparisons) do. Making ValueComparison implement Optimizable would be a significant performance improvement but changes query execution behavior and requires careful XQTS regression testing. Deferred to a dedicated optimizer overhaul tasking.

Examples

(: Quick timing :)
util:time(collection("/db/data")//title, "title lookup")
(: Logs: title lookup — 42.3ms :)

(: Structured measurement :)
let $r := util:track(collection("/db/data")//title)
return "Found " || count($r?value) || " titles in " || $r?time

(: Query plan :)
util:explain('for $x in 1 to 10 where $x > 5 return $x * 2')
(: Returns: <explain><for variable="$x"><in>...</in>...</for></explain> :)

(: Full profiling :)
let $p := util:profile('collection("/db/data")//book[@year > 2020]')
return ($p?time, count($p?result), $p?plan, $p?stats)

Test Plan

  • XQSuite: 31 tests in profiling.xql (94/94 total util tests pass)
  • Manual testing with indexed collections for util:index-report() and util:profile() stats
  • Verify no regressions in existing util function tests

🤖 Generated with Claude Code

joewiz and others added 4 commits March 28, 2026 15:15
…util:track()

Phase 1 of query profiling visibility (inspired by BaseX prof: module):

util:time($expr) / util:time($expr, $label):
  Pass-through wrapper that measures and logs execution time.
  Returns the expression result unchanged.

util:memory($expr) / util:memory($expr, $label):
  Same pattern for memory measurement. Logs the memory delta
  during expression evaluation.

util:track($expr) / util:track($expr, $label):
  Returns map { "time": xs:dayTimeDuration, "memory": xs:integer,
  "value": item()* }. Most useful of the three — combines time
  and memory measurement in a structured result.

All registered in UtilModule.java. 13 XQSuite tests in profiling.xql.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 of query profiling: util:explain($query) returns the compiled
expression tree as XML, showing the post-optimization query plan.

FunExplain.java:
- Compiles query string using the same pattern as util:compile()
- Runs AnalyzeContextInfo for optimizer annotations
- Serializes tree via QueryPlanSerializer visitor
- Returns root <explain> element

QueryPlanSerializer.java:
- Extends DefaultExpressionVisitor to walk the expression tree
- Emits XML elements for each expression type:
  <flwor>, <for>, <let>, <where>, <return>, <order-by>, <group-by>,
  <path>, <step>, <predicate>, <filter>, <comparison>,
  <function-call>, <builtin-function>, <user-function>,
  <variable>, <if>, <union>, <intersect>, <try-catch>,
  <element-constructor>, <text-constructor>, etc.
- Includes @axis, @test, @variable, @name, @Arity, @operator,
  @line, @column attributes where applicable

Example:
  util:explain('for $x in 1 to 10 where $x > 5 return $x')
  → <explain><for variable="$x"><in>...</in>...</for></explain>

7 new XQSuite tests for explain (83/83 total util tests pass).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 of query profiling: util:profile($query) executes a query with
profiling enabled and returns a map combining:

- result: the actual query result
- time: xs:dayTimeDuration of execution
- memory: xs:integer bytes delta during execution
- plan: element(explain) — the compiled expression tree (from Phase 2)
- stats: element() — profiler statistics XML from PerformanceStatsImpl
  (function calls, index usage, optimizations)

FunProfile.java:
- Compiles and analyzes query using the same pattern as util:explain()
- Enables eXist's built-in Profiler at verbosity=10 before execution
- Captures timing via System.nanoTime(), memory via Runtime
- Serializes the expression tree via QueryPlanSerializer
- Serializes profiler stats via PerformanceStatsImpl.serialize()
- Packages everything into a MapType result

Two signatures:
- util:profile($query as xs:string) as map(*)
- util:profile($query, $module-load-path) as map(*)

9 new XQSuite tests (92/92 total util tests pass).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3.2: Per-query scoping for PerformanceStats
- Add Profiler.getPerformanceStats() to expose per-profiler stats
- FunProfile now reads from the per-query profiler's stats instead
  of the global BrokerPool stats, ensuring correct data under
  concurrent load

Phase 4.1: Optimizer decision logging
- Add DEBUG-level logging in GeneralComparison.analyze() showing
  when index optimization is applied or skipped, including the
  expression text, QName, and optimization type

Phase 5.1: util:index-report($query) as element()
- Execute a query with profiling enabled and return an XML report
  of index usage and optimizations from the per-query PerformanceStats
- Uses the same per-query profiler isolation as util:profile()

2 new XQSuite tests (94/94 total pass).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joewiz joewiz requested a review from a team as a code owner March 28, 2026 20:23
@joewiz
Copy link
Copy Markdown
Member Author

joewiz commented Apr 6, 2026

[This comment was co-authored with Claude Code. -Joe]

Closing — superseded by #6208 (v2/query-profiling).

This work has been consolidated into a clean v2/ branch as part of the eXist-db 7.0 PR reorganization. The new PR includes all commits from this PR plus additional related work, with reviewer feedback incorporated where applicable. See the reviewer guide for the full context.

@joewiz joewiz closed this Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant