From c73e74711970105308b1bb10b8478816384009eb Mon Sep 17 00:00:00 2001 From: Finn Carroll Date: Thu, 14 May 2026 15:12:00 -0700 Subject: [PATCH] Wire analytics engine explain to return stage profiling via executeWithProfile AnalyticsExecutionEngine.explain() now calls executeWithProfile on the analytics engine's QueryPlanExecutor, executing the query and capturing per-stage timing from the coordinator's perspective. The resulting QueryProfile is attached to ExplainResponseNodeV2 and serialized in the /_plugins/_ppl/_explain response. Changes: - ExplainResponseNodeV2: add QueryProfile field with backward-compatible 3-arg constructor - ExplainResponse.normalizeLf(): preserve profile field when normalizing line endings - AnalyticsExecutionEngine.explain(): call executeWithProfile, attach profile to response. Falls back to plan-only on failure. Signed-off-by: Finn Carroll --- .../sql/executor/ExecutionEngine.java | 9 ++++- .../analytics/AnalyticsExecutionEngine.java | 40 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/executor/ExecutionEngine.java b/core/src/main/java/org/opensearch/sql/executor/ExecutionEngine.java index da8eae41355..0a678c0c84f 100644 --- a/core/src/main/java/org/opensearch/sql/executor/ExecutionEngine.java +++ b/core/src/main/java/org/opensearch/sql/executor/ExecutionEngine.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; import org.apache.calcite.rel.RelNode; +import org.opensearch.analytics.exec.profile.QueryProfile; import org.opensearch.sql.ast.statement.ExplainMode; import org.opensearch.sql.calcite.CalcitePlanContext; import org.opensearch.sql.common.response.ResponseListener; @@ -134,7 +135,8 @@ public static ExplainResponse normalizeLf(ExplainResponse response) { new ExecutionEngine.ExplainResponseNodeV2( normalizeLf(calcite.getLogical()), normalizeLf(calcite.getPhysical()), - normalizeLf(calcite.getExtended()))); + normalizeLf(calcite.getExtended()), + calcite.getProfile())); } return response; } @@ -160,5 +162,10 @@ class ExplainResponseNodeV2 { private final String logical; private final String physical; private final String extended; + private final QueryProfile profile; + + public ExplainResponseNodeV2(String logical, String physical, String extended) { + this(logical, physical, extended, null); + } } } diff --git a/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java b/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java index ddfe5fd3556..da1d81b6f58 100644 --- a/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java +++ b/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java @@ -14,6 +14,8 @@ import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeField; import org.opensearch.analytics.exec.QueryPlanExecutor; +import org.opensearch.analytics.exec.profile.ProfiledResult; +import org.opensearch.analytics.exec.profile.QueryProfile; import org.opensearch.core.action.ActionListener; import org.opensearch.sql.ast.statement.ExplainMode; import org.opensearch.sql.calcite.CalcitePlanContext; @@ -108,14 +110,36 @@ public void explain( ExplainMode mode, CalcitePlanContext context, ResponseListener listener) { - try { - String logical = RelOptUtil.toString(plan, mode.toExplainLevel()); - ExplainResponse response = - new ExplainResponse(new ExplainResponseNodeV2(logical, null, null)); - listener.onResponse(ExplainResponse.normalizeLf(response)); - } catch (Exception e) { - listener.onFailure(e); - } + String logical = RelOptUtil.toString(plan, mode.toExplainLevel()); + + planExecutor.executeWithProfile( + plan, + null, + new ActionListener<>() { + @Override + public void onResponse(ProfiledResult result) { + try { + QueryProfile profile = result.profile(); + ExplainResponse response = + new ExplainResponse(new ExplainResponseNodeV2(logical, null, null, profile)); + listener.onResponse(ExplainResponse.normalizeLf(response)); + } catch (Exception e) { + listener.onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + // Fall back to plan-only explain if profiling fails + try { + ExplainResponse response = + new ExplainResponse(new ExplainResponseNodeV2(logical, null, null, null)); + listener.onResponse(ExplainResponse.normalizeLf(response)); + } catch (Exception ex) { + listener.onFailure(ex); + } + } + }); } private List convertRows(Iterable rows, List fields) {