This is an automated email from the ASF dual-hosted git repository.
yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 236401b2dc0 Add totalMemAllocatedBytes to multi-stage query responses
(#19816)
236401b2dc0 is described below
commit 236401b2dc0f64cf5ce700920769751479691511
Author: Yash Mayya <[email protected]>
AuthorDate: Thu Oct 15 13:09:11 2026 -0700
Add totalMemAllocatedBytes to multi-stage query responses (#19816)
---
.../MultiStageBrokerRequestHandler.java | 23 +++++++++++---
.../MultiStageBrokerRequestHandlerTest.java | 37 ++++++++++++++++++++++
.../response/broker/BrokerResponseNativeV2.java | 19 +++++++++--
.../MultiNodesOfflineClusterIntegrationTest.java | 30 ++++++++++++++++++
.../query/runtime/operator/MultiStageOperator.java | 6 ++++
5 files changed, 107 insertions(+), 8 deletions(-)
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
index adf76c39c2e..007d6c82a81 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
@@ -1181,6 +1181,23 @@ public class MultiStageBrokerRequestHandler extends
BaseBrokerRequestHandler {
return stagePlans;
}
+ @VisibleForTesting
+ static void mergeStageStats(BrokerResponseNativeV2 brokerResponse,
+ List<MultiStageQueryStats.StageStats.Closed> queryStats) {
+ for (MultiStageQueryStats.StageStats.Closed stageStats : queryStats) {
+ if (stageStats != null) { // for example pipeline breaker may not have
stats
+ stageStats.forEach((type, stats) -> type.mergeInto(brokerResponse,
stats));
+ if (!stageStats.isEmpty()) {
+ // The stage root's allocation covers all the operators of its op
chain. It is read by name because stage
+ // roots have different stat key types. Leaf threads and pipeline
breakers are added in
+ // MultiStageOperator.Type#mergeInto.
+ brokerResponse.addTotalMemAllocatedBytes(
+
stageStats.getLastOperatorStats().getUnsafe("ALLOCATED_MEMORY_BYTES", 0L));
+ }
+ }
+ }
+ }
+
private void fillOldBrokerResponseStats(BrokerResponseNativeV2
brokerResponse,
List<MultiStageQueryStats.StageStats.Closed> queryStats,
DispatchableSubPlan dispatchableSubPlan,
@Nullable List<QueryDispatcher.QueryResult.StageCoverage> stageCoverage,
@@ -1191,11 +1208,7 @@ public class MultiStageBrokerRequestHandler extends
BaseBrokerRequestHandler {
MultiStageStatsTreeBuilder treeBuilder = new
MultiStageStatsTreeBuilder(queryStageMap, queryStats,
stageStatsTrees);
brokerResponse.setStageStats(treeBuilder.jsonStatsByStage(0));
- for (MultiStageQueryStats.StageStats.Closed stageStats : queryStats) {
- if (stageStats != null) { // for example pipeline breaker may not have
stats
- stageStats.forEach((type, stats) -> type.mergeInto(brokerResponse,
stats));
- }
- }
+ mergeStageStats(brokerResponse, queryStats);
// Broker-pruned segments are computed during routing in WorkerManager,
not reported by servers.
// Inject the count directly since it doesn't flow through the
LeafOperator stat pipeline.
long numSegmentsPrunedByBroker =
dispatchableSubPlan.getNumSegmentsPrunedByBroker();
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
index d2d99c2ebbd..d13a209398f 100644
---
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
@@ -30,6 +30,7 @@ import org.apache.pinot.broker.api.AccessControl;
import org.apache.pinot.broker.broker.AllowAllAccessControlFactory;
import org.apache.pinot.broker.queryquota.QueryQuotaManager;
import org.apache.pinot.common.config.provider.TableCache;
+import org.apache.pinot.common.datatable.StatMap;
import org.apache.pinot.common.failuredetector.FailureDetector;
import org.apache.pinot.common.metrics.BrokerMetrics;
import org.apache.pinot.common.response.BrokerResponse;
@@ -42,6 +43,12 @@ import
org.apache.pinot.query.planner.physical.DispatchablePlanFragment;
import org.apache.pinot.query.planner.physical.DispatchableSubPlan;
import org.apache.pinot.query.planner.physical.PinotDispatchPlanner;
import org.apache.pinot.query.routing.WorkerManager;
+import org.apache.pinot.query.runtime.operator.BaseMailboxReceiveOperator;
+import org.apache.pinot.query.runtime.operator.LeafOperator;
+import org.apache.pinot.query.runtime.operator.MailboxSendOperator;
+import org.apache.pinot.query.runtime.operator.MultiStageOperator;
+import org.apache.pinot.query.runtime.plan.MultiStageQueryStats;
+import org.apache.pinot.query.runtime.plan.pipeline.PipelineBreakerOperator;
import org.apache.pinot.query.service.dispatch.QueryDispatcher;
import org.apache.pinot.spi.accounting.ThreadAccountantUtils;
import org.apache.pinot.spi.auth.broker.RequesterIdentity;
@@ -199,6 +206,36 @@ public class MultiStageBrokerRequestHandlerTest extends
QueryEnvironmentTestBase
"Per-query SET must take precedence over the broker default");
}
+ @Test
+ public void testMergeStageStatsTotalMemAllocatedBytes() {
+ StatMap<BaseMailboxReceiveOperator.StatKey> rootReceive = new
StatMap<>(BaseMailboxReceiveOperator.StatKey.class)
+ .merge(BaseMailboxReceiveOperator.StatKey.ALLOCATED_MEMORY_BYTES,
100_000L);
+ StatMap<BaseMailboxReceiveOperator.StatKey> breakerReceive =
+ new StatMap<>(BaseMailboxReceiveOperator.StatKey.class)
+ .merge(BaseMailboxReceiveOperator.StatKey.ALLOCATED_MEMORY_BYTES,
1L);
+ StatMap<PipelineBreakerOperator.StatKey> breaker = new
StatMap<>(PipelineBreakerOperator.StatKey.class)
+ .merge(PipelineBreakerOperator.StatKey.ALLOCATED_MEMORY_BYTES, 10L);
+ StatMap<LeafOperator.StatKey> leaf = new
StatMap<>(LeafOperator.StatKey.class)
+ .merge(LeafOperator.StatKey.ALLOCATED_MEMORY_BYTES, 100L)
+ .merge(LeafOperator.StatKey.THREAD_MEM_ALLOCATED_BYTES, 1_000L);
+ StatMap<MailboxSendOperator.StatKey> send = new
StatMap<>(MailboxSendOperator.StatKey.class)
+ .merge(MailboxSendOperator.StatKey.ALLOCATED_MEMORY_BYTES, 10_000L);
+
+ // Stage 1 is a leaf stage with a pipeline breaker, flattened the same way
in legacy and stream stats modes
+ BrokerResponseNativeV2 brokerResponse = new BrokerResponseNativeV2();
+ MultiStageBrokerRequestHandler.mergeStageStats(brokerResponse, List.of(
+ new
MultiStageQueryStats.StageStats.Closed(List.of(MultiStageOperator.Type.MAILBOX_RECEIVE),
+ List.of(rootReceive)),
+ new MultiStageQueryStats.StageStats.Closed(
+ List.of(MultiStageOperator.Type.MAILBOX_RECEIVE,
MultiStageOperator.Type.PIPELINE_BREAKER,
+ MultiStageOperator.Type.LEAF,
MultiStageOperator.Type.MAILBOX_SEND),
+ List.of(breakerReceive, breaker, leaf, send))));
+
+ // Op chain roots (stage roots and the breaker) plus the leaf threads, but
not the breaker's receive or the leaf's
+ // own op chain allocation, which their roots already include
+ assertEquals(brokerResponse.getTotalMemAllocatedBytes(), 100_000L +
10_000L + 1_000L + 10L);
+ }
+
@Test
public void
testApplyBrokerDefaultQueryOptionsNoStreamingDistinctInjectionWhenConfigUnset()
throws Exception {
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/response/broker/BrokerResponseNativeV2.java
b/pinot-common/src/main/java/org/apache/pinot/common/response/broker/BrokerResponseNativeV2.java
index 827c800d037..e5738163357 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/response/broker/BrokerResponseNativeV2.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/response/broker/BrokerResponseNativeV2.java
@@ -53,9 +53,10 @@ import org.apache.pinot.common.response.ProcessingException;
"realtimeResponseSerializationCpuTimeNs", "offlineTotalCpuTimeNs",
"realtimeTotalCpuTimeNs",
"explainPlanNumEmptyFilterSegments",
"explainPlanNumMatchAllFilterSegments", "traceInfo", "tablesQueried",
"offlineThreadMemAllocatedBytes", "realtimeThreadMemAllocatedBytes",
"offlineResponseSerMemAllocatedBytes",
- "realtimeResponseSerMemAllocatedBytes", "offlineTotalMemAllocatedBytes",
"realtimeTotalMemAllocatedBytes", "pools",
- "rlsFiltersApplied", "approximateFunctionApplied", "groupsTrimmed",
"mseLiteLeafStageLimitReached",
- "mseLiteLeafStageEffectiveLimit", "mseLiteFanOutAdjustedLimitApplied",
"responseMetadata"
+ "realtimeResponseSerMemAllocatedBytes", "offlineTotalMemAllocatedBytes",
"realtimeTotalMemAllocatedBytes",
+ "totalMemAllocatedBytes", "pools", "rlsFiltersApplied",
"approximateFunctionApplied", "groupsTrimmed",
+ "mseLiteLeafStageLimitReached", "mseLiteLeafStageEffectiveLimit",
"mseLiteFanOutAdjustedLimitApplied",
+ "responseMetadata"
})
public class BrokerResponseNativeV2 implements BrokerResponse {
private final StatMap<StatKey> _brokerStats = new StatMap<>(StatKey.class);
@@ -81,6 +82,7 @@ public class BrokerResponseNativeV2 implements BrokerResponse
{
/// virtual rows can be generated. For example, in a join query, the number
of rows can be more than the number of
/// rows in the table.
private long _maxRowsInOperator;
+ private long _totalMemAllocatedBytes;
private String _requestId;
private String _clientRequestId;
private String _brokerId;
@@ -272,6 +274,17 @@ public class BrokerResponseNativeV2 implements
BrokerResponse {
_maxRowsInOperator = Math.max(_maxRowsInOperator, maxRows);
}
+ /// Returns the heap bytes allocated by the query across all its stages and
workers, as reported in the stage stats.
+ /// This is the total allocated (including short-lived objects), not the
peak heap used. It is 0 unless thread
+ /// allocated bytes measurement is enabled on the brokers and servers.
+ public long getTotalMemAllocatedBytes() {
+ return _totalMemAllocatedBytes;
+ }
+
+ public void addTotalMemAllocatedBytes(long bytes) {
+ _totalMemAllocatedBytes += bytes;
+ }
+
@Override
public long getTimeUsedMs() {
return _timeUsedMs;
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiNodesOfflineClusterIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiNodesOfflineClusterIntegrationTest.java
index c5849bba8cc..04774cf461c 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiNodesOfflineClusterIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MultiNodesOfflineClusterIntegrationTest.java
@@ -326,6 +326,36 @@ public class MultiNodesOfflineClusterIntegrationTest
extends OfflineClusterInteg
assertEquals(result.get("numServersQueried").intValue(), 1);
}
+ @Test
+ public void testMultiStageTotalMemAllocatedBytes()
+ throws Exception {
+ setUseMultiStageQueryEngine(true);
+
+ // The semi-join query uses a pipeline breaker
+ for (String query : List.of("SELECT Carrier, COUNT(*) FROM mytable GROUP
BY Carrier",
+ "SELECT Carrier FROM mytable WHERE Origin IN (SELECT Origin FROM
mytable WHERE ArrDelay > 0) LIMIT 10")) {
+ JsonNode result = postQuery("SET streamStats = false; " + query);
+ long totalMemAllocatedBytes =
result.path("totalMemAllocatedBytes").asLong();
+ assertTrue(totalMemAllocatedBytes > 0);
+ assertEquals(totalMemAllocatedBytes,
sumAllocatedBytes(result.get("stageStats"), true));
+ }
+ }
+
+ /// Sums the allocation of each op chain root (the top node, each
MAILBOX_SEND and each pipeline breaker) and of the
+ /// single-stage threads of each LEAF. The legacy (non-stream) stats tree
renders a pipeline breaker as the
+ /// MAILBOX_RECEIVE child of its LEAF, but that node carries the stats of
the PIPELINE_BREAKER operator itself.
+ private static long sumAllocatedBytes(JsonNode node, boolean isOpChainRoot) {
+ long bytes = node.path("threadMemAllocatedBytes").asLong();
+ if (isOpChainRoot) {
+ bytes += node.path("allocatedMemoryBytes").asLong();
+ }
+ boolean isLeaf = node.path("type").asText().equals("LEAF");
+ for (JsonNode child : node.path("children")) {
+ bytes += sumAllocatedBytes(child, isLeaf ||
child.path("type").asText().equals("MAILBOX_SEND"));
+ }
+ return bytes;
+ }
+
// Disabled because segments might not be server partitioned with multiple
servers
@Test(enabled = false)
@Override
diff --git
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultiStageOperator.java
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultiStageOperator.java
index 88dafd2e012..c702d42a3c4 100644
---
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultiStageOperator.java
+++
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultiStageOperator.java
@@ -422,6 +422,9 @@ public abstract class MultiStageOperator implements
Operator<MseBlock>, AutoClos
@SuppressWarnings("unchecked")
StatMap<LeafOperator.StatKey> stats = (StatMap<LeafOperator.StatKey>)
map;
response.mergeMaxRowsInOperator(stats.getLong(LeafOperator.StatKey.EMITTED_ROWS));
+ // The single-stage threads of the leaf are not covered by the stage
root's allocation, which
+ // MultiStageBrokerRequestHandler#mergeStageStats adds
+
response.addTotalMemAllocatedBytes(stats.getLong(LeafOperator.StatKey.THREAD_MEM_ALLOCATED_BYTES));
StatMap<BrokerResponseNativeV2.StatKey> brokerStats = new
StatMap<>(BrokerResponseNativeV2.StatKey.class);
for (LeafOperator.StatKey statKey : stats.keySet()) {
@@ -495,6 +498,9 @@ public abstract class MultiStageOperator implements
Operator<MseBlock>, AutoClos
@SuppressWarnings("unchecked")
StatMap<PipelineBreakerOperator.StatKey> stats =
(StatMap<PipelineBreakerOperator.StatKey>) map;
response.mergeMaxRowsInOperator(stats.getLong(PipelineBreakerOperator.StatKey.EMITTED_ROWS));
+ // Pipeline breakers run in their own op chain, so they are not
covered by the stage root's allocation, which
+ // MultiStageBrokerRequestHandler#mergeStageStats adds
+
response.addTotalMemAllocatedBytes(stats.getLong(PipelineBreakerOperator.StatKey.ALLOCATED_MEMORY_BYTES));
}
},
SORT_OR_LIMIT(10, SortOperator.StatKey.class) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]