[Enhancement] optimize analyze profile format (backport #63326) (#63396)

Co-authored-by: Murphy <96611012+murphyatwork@users.noreply.github.com>
This commit is contained in:
mergify[bot] 2025-09-23 03:14:16 +00:00 committed by GitHub
parent 449a3ab2a6
commit cc892a7196
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 823 additions and 2 deletions

View File

@ -34,6 +34,7 @@
package com.starrocks.common.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@ -136,6 +137,16 @@ public class RuntimeProfile {
return addCounter(name, type, strategy, ROOT_COUNTER);
}
@VisibleForTesting
public void addCounter(String name, String parentName, Counter counter) {
this.counterMap.put(name, Pair.create(counter, parentName));
if (!childCounterMap.containsKey(parentName)) {
childCounterMap.putIfAbsent(parentName, Sets.newConcurrentHashSet());
}
Set<String> childNames = childCounterMap.get(parentName);
childNames.add(name);
}
public Counter addCounter(String name, TUnit type, TCounterStrategy strategy, String parentName) {
if (strategy == null) {
strategy = Counter.createStrategy(type);

View File

@ -453,7 +453,10 @@ public class ExplainAnalyzer {
// 7. Non default Variables
String sessionVariables = summaryProfile.getInfoString("NonDefaultSessionVariables");
Map<String, SessionVariable.NonDefaultValue> variables = Maps.newTreeMap();
variables.putAll(SessionVariable.NonDefaultValue.parseFrom(sessionVariables));
var map = SessionVariable.NonDefaultValue.parseFrom(sessionVariables);
if (map != null) {
variables.putAll(map);
}
if (MapUtils.isNotEmpty(variables)) {
appendSummaryLine("NonDefaultVariables:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
@ -936,13 +939,257 @@ public class ExplainAnalyzer {
if (CollectionUtils.isNotEmpty(mergedUniqueMetrics.getChildCounterMap().get(RuntimeProfile.ROOT_COUNTER))) {
appendDetailLine("Counters:");
metricTraverser.accept(name -> true, true);
boolean applied = appendGroupedMetricsByOperator(mergedUniqueMetrics, nodeInfo);
if (!applied) {
metricTraverser.accept(name -> true, true);
}
}
popIndent(); // metric indent
}
}
private boolean appendGroupedMetricsByOperator(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo) {
if (nodeInfo.element.instanceOf(JoinNode.class)) {
appendGroupedMetricsForJoin(uniqueMetrics, nodeInfo);
} else if (nodeInfo.element.instanceOf(AggregationNode.class)) {
appendGroupedMetricsForAggregate(uniqueMetrics, nodeInfo);
} else if (nodeInfo.element.instanceOf(ScanNode.class)) {
appendGroupedMetricsForScan(uniqueMetrics, nodeInfo);
} else {
appendGroupedMetricsForOthers(uniqueMetrics, nodeInfo);
}
return true;
}
private void appendGroupedMetricsForJoin(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo) {
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendDetailLine("HashTable:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "BuildBuckets");
appendMetric(uniqueMetrics, nodeInfo, "BuildKeysPerBucket%");
appendMetric(uniqueMetrics, nodeInfo, "BuildHashTableTime");
appendMetric(uniqueMetrics, nodeInfo, "BuildConjunctEvaluateTime");
appendMetric(uniqueMetrics, nodeInfo, "HashTableMemoryUsage");
appendMetric(uniqueMetrics, nodeInfo, "PartitionNums");
appendMetric(uniqueMetrics, nodeInfo, "PartitionProbeOverhead");
popIndent();
appendDetailLine("ProbeSide:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "SearchHashTableTime");
appendMetric(uniqueMetrics, nodeInfo, "probeCount");
appendMetric(uniqueMetrics, nodeInfo, "ProbeConjunctEvaluateTime");
appendMetric(uniqueMetrics, nodeInfo, "CopyRightTableChunkTime");
appendMetric(uniqueMetrics, nodeInfo, "OtherJoinConjunctEvaluateTime");
appendMetric(uniqueMetrics, nodeInfo, "OutputBuildColumnTime");
appendMetric(uniqueMetrics, nodeInfo, "OutputProbeColumnTime");
appendMetric(uniqueMetrics, nodeInfo, "WhereConjunctEvaluateTime");
popIndent();
appendDetailLine("RuntimeFilter:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "RuntimeFilterBuildTime");
appendMetric(uniqueMetrics, nodeInfo, "RuntimeFilterNum");
appendMetric(uniqueMetrics, nodeInfo, "PartialRuntimeMembershipFilterBytes");
popIndent();
appendGroupedMetricsOthers(uniqueMetrics, nodeInfo, getJoinKnownMetrics());
popIndent();
}
private void appendGroupedMetricsForAggregate(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo) {
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendDetailLine("Aggregation:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "AggFuncComputeTime");
appendMetric(uniqueMetrics, nodeInfo, "ExprComputeTime");
appendMetric(uniqueMetrics, nodeInfo, "ExprReleaseTime");
appendMetric(uniqueMetrics, nodeInfo, "HashTableSize");
appendMetric(uniqueMetrics, nodeInfo, "HashTableMemoryUsage");
popIndent();
appendDetailLine("Memory Management:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "ChunkBufferPeakMem");
appendMetric(uniqueMetrics, nodeInfo, "ChunkBufferPeakSize");
appendMetric(uniqueMetrics, nodeInfo, "StateAllocate");
appendMetric(uniqueMetrics, nodeInfo, "StateDestroy");
popIndent();
appendDetailLine("Result Processing:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "GetResultsTime");
appendMetric(uniqueMetrics, nodeInfo, "ResultAggAppendTime");
appendMetric(uniqueMetrics, nodeInfo, "ResultGroupByAppendTime");
appendMetric(uniqueMetrics, nodeInfo, "ResultIteratorTime");
popIndent();
appendDetailLine("Data Flow:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "InputRowCount");
appendMetric(uniqueMetrics, nodeInfo, "PassThroughRowCount");
appendMetric(uniqueMetrics, nodeInfo, "StreamingTime");
appendMetric(uniqueMetrics, nodeInfo, "RowsReturned");
popIndent();
appendGroupedMetricsOthers(uniqueMetrics, nodeInfo, getAggregateKnownMetrics());
popIndent();
}
private void appendGroupedMetricsForScan(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo) {
pushIndent(GraphElement.LEAF_METRIC_INDENT);
// Scan Filters & Row Processing
appendDetailLine("ScanFilters:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendFilterMetrics(uniqueMetrics, nodeInfo, "ShortKeyFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "BitmapIndexFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "BloomFilterFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "ZoneMapFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "PredFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "GinFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "VectorIndexFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "DelVecFilter");
appendFilterMetrics(uniqueMetrics, nodeInfo, "RuntimeFilter");
popIndent();
appendDetailLine("RowProcessing:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "RawRowsRead");
appendMetric(uniqueMetrics, nodeInfo, "RowsRead");
appendMetric(uniqueMetrics, nodeInfo, "DictDecode");
appendMetric(uniqueMetrics, nodeInfo, "DictDecodeCount");
appendMetric(uniqueMetrics, nodeInfo, "ChunkCopy");
popIndent(); // RowProcessing indent
appendDetailLine("IOMetrics:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "IOTime");
appendMetric(uniqueMetrics, nodeInfo, "BytesRead");
appendMetric(uniqueMetrics, nodeInfo, "CompressedBytesRead");
appendMetric(uniqueMetrics, nodeInfo, "UncompressedBytesRead");
appendMetric(uniqueMetrics, nodeInfo, "ReadPagesNum");
appendMetric(uniqueMetrics, nodeInfo, "CachedPagesNum");
appendMetric(uniqueMetrics, nodeInfo, "BlockFetch");
appendMetric(uniqueMetrics, nodeInfo, "BlockFetchCount");
appendMetric(uniqueMetrics, nodeInfo, "BlockSeek");
appendMetric(uniqueMetrics, nodeInfo, "BlockSeekCount");
appendMetric(uniqueMetrics, nodeInfo, "DecompressTime");
popIndent();
appendDetailLine("SegmentProcessing:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "TabletCount");
appendMetric(uniqueMetrics, nodeInfo, "SegmentsReadCount");
appendMetric(uniqueMetrics, nodeInfo, "RowsetsReadCount");
appendMetric(uniqueMetrics, nodeInfo, "TotalColumnsDataPageCount");
appendMetric(uniqueMetrics, nodeInfo, "ColumnIteratorInit");
appendMetric(uniqueMetrics, nodeInfo, "BitmapIndexIteratorInit");
appendMetric(uniqueMetrics, nodeInfo, "FlatJsonInit");
appendMetric(uniqueMetrics, nodeInfo, "FlatJsonMerge");
popIndent();
appendDetailLine("IOTask:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "IOTaskExecTime");
appendMetric(uniqueMetrics, nodeInfo, "IOTaskWaitTime");
appendMetric(uniqueMetrics, nodeInfo, "SubmitTaskCount");
appendMetric(uniqueMetrics, nodeInfo, "SubmitTaskTime");
appendMetric(uniqueMetrics, nodeInfo, "PrepareChunkSourceTime");
appendMetric(uniqueMetrics, nodeInfo, "MorselsCount");
appendMetric(uniqueMetrics, nodeInfo, "PeakIOTasks");
appendMetric(uniqueMetrics, nodeInfo, "PeakScanTaskQueueSize");
popIndent();
appendDetailLine("IOBuffer:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendMetric(uniqueMetrics, nodeInfo, "PeakChunkBufferMemoryUsage");
appendMetric(uniqueMetrics, nodeInfo, "PeakChunkBufferSize");
appendMetric(uniqueMetrics, nodeInfo, "ChunkBufferCapacity");
appendMetric(uniqueMetrics, nodeInfo, "DefaultChunkBufferCapacity");
popIndent();
appendGroupedMetricsOthers(uniqueMetrics, nodeInfo, getScanKnownMetrics());
popIndent(); // main indent
}
private void appendFilterMetrics(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo, String filterName) {
Counter timeCounter = uniqueMetrics.getCounter(filterName);
Counter rowsCounter = uniqueMetrics.getCounter(filterName + "Rows");
if (timeCounter == null && rowsCounter == null) {
return;
}
if (rowsCounter == null || rowsCounter.getValue() == 0L) {
return;
}
List<Object> items = Lists.newArrayList();
items.add(filterName);
items.add(": ");
items.add("Rows: ");
items.add(rowsCounter);
if (timeCounter != null) {
items.add(", ");
}
if (timeCounter != null) {
items.add("Time: ");
items.add(timeCounter);
Counter minCounter = uniqueMetrics.getCounter(RuntimeProfile.MERGED_INFO_PREFIX_MIN + filterName);
Counter maxCounter = uniqueMetrics.getCounter(RuntimeProfile.MERGED_INFO_PREFIX_MAX + filterName);
if (minCounter != null || maxCounter != null) {
items.add(" [");
items.add("min=");
items.add(minCounter);
items.add(", max=");
items.add(maxCounter);
items.add("]");
}
}
appendDetailLine(items.toArray());
}
private void appendMetric(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo, String name) {
Counter counter = uniqueMetrics.getCounter(name);
if (counter == null) {
return;
}
Counter minCounter = uniqueMetrics.getCounter(RuntimeProfile.MERGED_INFO_PREFIX_MIN + name);
Counter maxCounter = uniqueMetrics.getCounter(RuntimeProfile.MERGED_INFO_PREFIX_MAX + name);
boolean needHighlight = colorExplainOutput && nodeInfo.isTimeConsumingMetric(uniqueMetrics, name);
List<Object> items = Lists.newArrayList();
if (needHighlight) {
items.add(getBackGround());
}
items.add(name);
items.add(": ");
items.add(counter);
if (minCounter != null || maxCounter != null) {
items.add(" [");
items.add("min=");
items.add(minCounter);
items.add(", max=");
items.add(maxCounter);
items.add("]");
}
if (needHighlight) {
items.add(ANSI_RESET);
}
appendDetailLine(items.toArray());
}
private void appendDetailMetric(NodeInfo nodeInfo, RuntimeProfile uniqueMetrics, String name,
boolean enableHighlight) {
if (name.startsWith(RuntimeProfile.MERGED_INFO_PREFIX_MIN)
@ -1397,4 +1644,103 @@ public class ExplainAnalyzer {
}
}
}
private void appendGroupedMetricsOthers(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo, Set<String> knownMetrics) {
Set<String> allMetrics = uniqueMetrics.getCounterMap().keySet();
Set<String> otherMetrics = Sets.newTreeSet();
for (String metric : allMetrics) {
if (!knownMetrics.contains(metric) &&
!metric.startsWith(RuntimeProfile.MERGED_INFO_PREFIX_MIN) &&
!metric.startsWith(RuntimeProfile.MERGED_INFO_PREFIX_MAX) &&
!EXCLUDE_DETAIL_METRIC_NAMES.contains(metric)) {
otherMetrics.add(metric);
}
}
if (!otherMetrics.isEmpty()) {
appendDetailLine("Others:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
for (String metric : otherMetrics) {
appendMetric(uniqueMetrics, nodeInfo, metric);
}
popIndent();
}
}
private void appendGroupedMetricsForOthers(RuntimeProfile uniqueMetrics, NodeInfo nodeInfo) {
pushIndent(GraphElement.LEAF_METRIC_INDENT);
appendDetailLine("Others:");
pushIndent(GraphElement.LEAF_METRIC_INDENT);
Set<String> allMetrics = uniqueMetrics.getCounterMap().keySet();
Set<String> otherMetrics = Sets.newTreeSet();
for (String metric : allMetrics) {
if (!metric.startsWith(RuntimeProfile.MERGED_INFO_PREFIX_MIN) &&
!metric.startsWith(RuntimeProfile.MERGED_INFO_PREFIX_MAX) &&
!EXCLUDE_DETAIL_METRIC_NAMES.contains(metric)) {
otherMetrics.add(metric);
}
}
for (String metric : otherMetrics) {
appendMetric(uniqueMetrics, nodeInfo, metric);
}
popIndent();
popIndent();
}
private Set<String> getJoinKnownMetrics() {
return Sets.newHashSet(
// HashTable group metrics
"BuildBuckets", "BuildKeysPerBucket%", "BuildHashTableTime", "BuildConjunctEvaluateTime",
"HashTableMemoryUsage", "PartitionNums", "PartitionProbeOverhead",
// ProbeSide group metrics
"SearchHashTableTime", "probeCount", "ProbeConjunctEvaluateTime", "CopyRightTableChunkTime",
"OtherJoinConjunctEvaluateTime", "OutputBuildColumnTime", "OutputProbeColumnTime",
"WhereConjunctEvaluateTime",
// RuntimeFilter group metrics
"RuntimeFilterBuildTime", "RuntimeFilterNum", "PartialRuntimeMembershipFilterBytes",
// Legacy metrics (kept for backward compatibility)
"BuildRows", "BuildTime", "BuildHashTableMemoryUsage", "HashBuckets", "HashCollisions",
"BuildSpillBytes", "BuildSpillTime", "ProbeRows", "ProbeTime", "JoinOutputRows",
"RowsAfterJoinFilter", "JoinConjunctEvaluateTime", "ProbeSpillBytes", "ProbeSpillTime",
"RuntimeFilterPushDownTime", "RuntimeFilterInputRows", "RuntimeFilterOutputRows"
);
}
private Set<String> getAggregateKnownMetrics() {
return Sets.newHashSet(
"AggInputRows", "AggOutputRows", "AggComputeTime", "HashAggBuildTime", "HashAggProbeTime",
"HashBuckets", "HashCollisions", "HashTableMemoryUsage", "SpillBytes", "SpillTime",
"DistinctInputRows", "DistinctOutputRows", "DistinctTime",
// Additional metrics from real data
"AggFuncComputeTime", "ChunkBufferPeakMem", "ChunkBufferPeakSize", "ExprComputeTime",
"ExprReleaseTime", "GetResultsTime", "HashTableSize", "InputRowCount", "PassThroughRowCount",
"ResultAggAppendTime", "ResultGroupByAppendTime", "ResultIteratorTime", "RowsReturned",
"StateAllocate", "StateDestroy", "StreamingTime"
);
}
private Set<String> getScanKnownMetrics() {
return Sets.newHashSet(
"ShortKeyFilter", "ShortKeyFilterRows", "BitmapIndexFilter", "BitmapIndexFilterRows",
"BloomFilterFilter", "BloomFilterFilterRows", "ZoneMapFilter", "ZoneMapFilterRows",
"PredFilter", "PredFilterRows", "GinFilter", "GinFilterRows", "VectorIndexFilter",
"VectorIndexFilterRows", "DelVecFilter", "DelVecFilterRows", "RuntimeFilter", "RuntimeFilterRows",
"RawRowsRead", "RowsRead", "DictDecode", "DictDecodeCount", "ChunkCopy",
"IOTime", "BytesRead", "CompressedBytesRead", "UncompressedBytesRead", "ReadPagesNum",
"CachedPagesNum", "BlockFetch", "BlockFetchCount", "BlockSeek", "BlockSeekCount", "DecompressTime",
"TabletCount", "SegmentsReadCount", "RowsetsReadCount", "TotalColumnsDataPageCount",
"ColumnIteratorInit", "BitmapIndexIteratorInit", "FlatJsonInit", "FlatJsonMerge",
"IOTaskExecTime", "IOTaskWaitTime", "SubmitTaskCount", "SubmitTaskTime", "PrepareChunkSourceTime",
"MorselsCount", "PeakIOTasks", "PeakScanTaskQueueSize", "PeakChunkBufferMemoryUsage",
"PeakChunkBufferSize", "ChunkBufferCapacity", "DefaultChunkBufferCapacity",
"CreateSegmentIter", "GetDelVec", "GetDeltaColumnGroup", "GetRowsets", "ReadPKIndex",
"ProcessVectorDistanceAndIdTime", "VectorSearchTime",
"PushdownAccessPaths", "PushdownPredicates"
);
}
}

View File

@ -0,0 +1,464 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.sql;
import com.starrocks.common.util.Counter;
import com.starrocks.common.util.ProfileManager;
import com.starrocks.common.util.ProfilingExecPlan;
import com.starrocks.common.util.RuntimeProfile;
import com.starrocks.planner.AggregationNode;
import com.starrocks.planner.JoinNode;
import com.starrocks.planner.ResultSink;
import com.starrocks.planner.ScanNode;
import com.starrocks.planner.SortNode;
import com.starrocks.thrift.TUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ExplainAnalyzerTest {
private RuntimeProfile mockProfile;
@BeforeEach
public void setUp() {
// Only initialize basic profile structure, each test will create its own operator profiles
mockProfile = new RuntimeProfile("Query");
// Create Summary profile
RuntimeProfile summaryProfile = new RuntimeProfile("Summary");
summaryProfile.addInfoString(ProfileManager.QUERY_ID, "test-query-id");
summaryProfile.addInfoString(ProfileManager.QUERY_STATE, "Finished");
summaryProfile.addInfoString("Query State", "Finished");
summaryProfile.addInfoString("NonDefaultSessionVariables", "");
summaryProfile.addCounter(ProfileManager.PROFILE_COLLECT_TIME, "Summary",
new Counter(TUnit.TIME_NS, null, 100000000));
mockProfile.addChild(summaryProfile);
// Create Planner profile
RuntimeProfile plannerProfile = new RuntimeProfile("Planner");
mockProfile.addChild(plannerProfile);
// Create Execution profile
RuntimeProfile executionProfile = new RuntimeProfile("Execution");
executionProfile.addCounter("FrontendProfileMergeTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 1000));
executionProfile.addCounter("QueryPeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 1024 * 1024));
executionProfile.addCounter("QueryAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 512 * 1024));
executionProfile.addCounter("QueryCumulativeOperatorTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 1000000));
executionProfile.addCounter("QueryCumulativeScanTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 500000));
executionProfile.addCounter("QueryCumulativeNetworkTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 100000));
executionProfile.addCounter("QueryPeakScheduleTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 50000));
mockProfile.addChild(executionProfile);
// Create Fragment profile
RuntimeProfile fragmentProfile = new RuntimeProfile("Fragment 0");
fragmentProfile.addCounter("BackendNum", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 1));
fragmentProfile.addCounter("InstancePeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 1024 * 1024));
fragmentProfile.addCounter("InstanceAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 512 * 1024));
fragmentProfile.addCounter("FragmentInstancePrepareTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 1000));
executionProfile.addChild(fragmentProfile);
}
@AfterEach
public void tearDown() {
mockProfile = null;
}
@Test
public void testScanOperator() throws NoSuchFieldException, IllegalAccessException {
// Create OLAP_SCAN operator profile
RuntimeProfile olapScanProfile = new RuntimeProfile("OLAP_SCAN (plan_node_id=0)");
olapScanProfile.addCounter("TotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 228000000));
olapScanProfile.addCounter("CPUTime", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 31000000));
olapScanProfile.addCounter("ScanTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 197000000));
olapScanProfile.addCounter("OutputRows", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 8553457));
// Create CommonMetrics profile
RuntimeProfile commonMetrics = new RuntimeProfile("CommonMetrics");
commonMetrics.addCounter("OperatorTotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 228000000));
commonMetrics.addCounter("PullRowNum", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 8553457));
commonMetrics.addCounter("OperatorPeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 1024 * 1024));
commonMetrics.addCounter("OperatorAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 512 * 1024));
olapScanProfile.addChild(commonMetrics);
// Create UniqueMetrics profile with scan-specific metrics
RuntimeProfile uniqueMetrics = new RuntimeProfile("UniqueMetrics");
uniqueMetrics.addCounter("ZoneMapFilter", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 28208));
uniqueMetrics.addCounter("ZoneMapFilterRows", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 37558774));
uniqueMetrics.addCounter("PredFilter", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 184998));
uniqueMetrics.addCounter("PredFilterRows", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 45617439));
uniqueMetrics.addCounter("ShortKeyFilter", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 3916));
uniqueMetrics.addCounter("ShortKeyFilterRows", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, -51182470));
uniqueMetrics.addCounter("RawRowsRead", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 54170896));
uniqueMetrics.addCounter("RowsRead", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 8553457));
uniqueMetrics.addCounter("IOTime", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 16403));
uniqueMetrics.addCounter("BytesRead", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 37 * 1024 * 1024));
uniqueMetrics.addCounter("TabletCount", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 13));
uniqueMetrics.addCounter("SegmentsReadCount", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 1921));
uniqueMetrics.addCounter("IOTaskExecTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 11372000));
uniqueMetrics.addCounter("PeakChunkBufferMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 12 * 1024 * 1024));
uniqueMetrics.addCounter("Unknown", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 12 * 1024 * 1024));
olapScanProfile.addChild(uniqueMetrics);
// Create pipeline profile structure
RuntimeProfile pipelineProfile = new RuntimeProfile("Pipeline 0");
pipelineProfile.addChild(olapScanProfile);
mockProfile.getChild("Execution").getChild("Fragment 0").addChild(pipelineProfile);
// Create mock plan with ScanNode
ProfilingExecPlan scanPlan = new ProfilingExecPlan();
Field level = ProfilingExecPlan.class.getDeclaredField("profileLevel");
level.setAccessible(true);
level.setInt(scanPlan, 1);
Field fragmentsField = ProfilingExecPlan.class.getDeclaredField("fragments");
fragmentsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<ProfilingExecPlan.ProfilingFragment> fragments =
(List<ProfilingExecPlan.ProfilingFragment>) fragmentsField.get(scanPlan);
ProfilingExecPlan.ProfilingElement root =
new ProfilingExecPlan.ProfilingElement(0, ScanNode.class);
ProfilingExecPlan.ProfilingElement sink =
new ProfilingExecPlan.ProfilingElement(-1, ResultSink.class);
fragments.add(new ProfilingExecPlan.ProfilingFragment(sink, root));
String result = ExplainAnalyzer.analyze(scanPlan, mockProfile, List.of(0), false);
assertTrue(result.contains("ScanFilters"), result);
assertTrue(result.contains("RowProcessing"), result);
assertTrue(result.contains("IOMetrics"), result);
assertTrue(result.contains("SegmentProcessing"), result);
assertTrue(result.contains("IOTask"), result);
assertTrue(result.contains("IOBuffer"), result);
assertTrue(result.contains("ZoneMapFilter: "), result);
assertTrue(result.contains("PredFilter: "), result);
assertTrue(result.contains("ShortKeyFilter:"), result);
assertTrue(result.contains("Others"), result);
}
@Test
public void testAggregationOperator() throws NoSuchFieldException, IllegalAccessException {
// Create mock profile for aggregation operator
RuntimeProfile aggProfile = new RuntimeProfile("HASH_AGG (plan_node_id=1)");
aggProfile.addCounter("TotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 150000000));
aggProfile.addCounter("CPUTime", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 120000000));
aggProfile.addCounter("OutputRows", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 100000));
// Create CommonMetrics profile
RuntimeProfile commonMetrics = new RuntimeProfile("CommonMetrics");
commonMetrics.addCounter("OperatorTotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 150000000));
commonMetrics.addCounter("PullRowNum", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 1000000));
commonMetrics.addCounter("OperatorPeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 64 * 1024 * 1024));
commonMetrics.addCounter("OperatorAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 32 * 1024 * 1024));
aggProfile.addChild(commonMetrics);
// Create UniqueMetrics profile with aggregation-specific metrics
RuntimeProfile uniqueMetrics = new RuntimeProfile("UniqueMetrics");
// Aggregation metrics
uniqueMetrics.addCounter("AggFuncComputeTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 80000000));
uniqueMetrics.addCounter("ExprComputeTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 20000000));
uniqueMetrics.addCounter("ExprReleaseTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 1000000));
uniqueMetrics.addCounter("HashTableSize", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 10000));
uniqueMetrics.addCounter("HashTableMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 16 * 1024 * 1024));
// Memory Management metrics
uniqueMetrics.addCounter("ChunkBufferPeakMem", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 8 * 1024 * 1024));
uniqueMetrics.addCounter("ChunkBufferPeakSize", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 100));
uniqueMetrics.addCounter("StateAllocate", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 5000000));
uniqueMetrics.addCounter("StateDestroy", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 2000000));
// Result Processing metrics
uniqueMetrics.addCounter("GetResultsTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 10000000));
uniqueMetrics.addCounter("ResultAggAppendTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 5000000));
uniqueMetrics.addCounter("ResultGroupByAppendTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 3000000));
uniqueMetrics.addCounter("ResultIteratorTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 2000000));
// Data Flow metrics
uniqueMetrics.addCounter("InputRowCount", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 1000000));
uniqueMetrics.addCounter("PassThroughRowCount", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 50000));
uniqueMetrics.addCounter("StreamingTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 15000000));
uniqueMetrics.addCounter("RowsReturned", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 100000));
aggProfile.addChild(uniqueMetrics);
// Create pipeline profile structure
RuntimeProfile pipelineProfile = new RuntimeProfile("Pipeline 1");
pipelineProfile.addChild(aggProfile);
mockProfile.getChild("Execution").getChild("Fragment 0").addChild(pipelineProfile);
// Create mock plan with AggregationNode
ProfilingExecPlan aggPlan = new ProfilingExecPlan();
Field level = ProfilingExecPlan.class.getDeclaredField("profileLevel");
level.setAccessible(true);
level.setInt(aggPlan, 1);
Field fragmentsField = ProfilingExecPlan.class.getDeclaredField("fragments");
fragmentsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<ProfilingExecPlan.ProfilingFragment> fragments =
(List<ProfilingExecPlan.ProfilingFragment>) fragmentsField.get(aggPlan);
ProfilingExecPlan.ProfilingElement root =
new ProfilingExecPlan.ProfilingElement(1, AggregationNode.class);
ProfilingExecPlan.ProfilingElement sink =
new ProfilingExecPlan.ProfilingElement(-1, ResultSink.class);
fragments.add(new ProfilingExecPlan.ProfilingFragment(sink, root));
String result = ExplainAnalyzer.analyze(aggPlan, mockProfile, List.of(1), false);
assertTrue(result.contains("Aggregation:"), result);
assertTrue(result.contains("Memory Management:"), result);
assertTrue(result.contains("Result Processing:"), result);
assertTrue(result.contains("Data Flow:"), result);
assertTrue(result.contains("AggFuncComputeTime: "), result);
assertTrue(result.contains("HashTableSize: "), result);
assertTrue(result.contains("InputRowCount: "), result);
}
@Test
public void testJoinOperator() throws NoSuchFieldException, IllegalAccessException {
// Create mock profile for join operator
RuntimeProfile joinProfile = new RuntimeProfile("HASH_JOIN (plan_node_id=2)");
joinProfile.addCounter("TotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 200000000));
joinProfile.addCounter("CPUTime", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 180000000));
joinProfile.addCounter("OutputRows", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 500000));
// Create CommonMetrics profile
RuntimeProfile commonMetrics = new RuntimeProfile("CommonMetrics");
commonMetrics.addCounter("OperatorTotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 200000000));
commonMetrics.addCounter("PullRowNum", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 2000000));
commonMetrics.addCounter("OperatorPeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 128 * 1024 * 1024));
commonMetrics.addCounter("OperatorAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 64 * 1024 * 1024));
joinProfile.addChild(commonMetrics);
// Create UniqueMetrics profile with join-specific metrics
RuntimeProfile uniqueMetrics = new RuntimeProfile("UniqueMetrics");
// HashTable metrics
uniqueMetrics.addCounter("BuildBuckets", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 100000));
uniqueMetrics.addCounter("BuildKeysPerBucket%", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 85));
uniqueMetrics.addCounter("BuildHashTableTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 50000000));
uniqueMetrics.addCounter("BuildConjunctEvaluateTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 10000000));
uniqueMetrics.addCounter("HashTableMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 32 * 1024 * 1024));
uniqueMetrics.addCounter("PartitionNums", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 4));
uniqueMetrics.addCounter("PartitionProbeOverhead", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 2000000));
// ProbeSide metrics
uniqueMetrics.addCounter("SearchHashTableTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 80000000));
uniqueMetrics.addCounter("probeCount", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 2000000));
uniqueMetrics.addCounter("ProbeConjunctEvaluateTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 15000000));
uniqueMetrics.addCounter("CopyRightTableChunkTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 10000000));
uniqueMetrics.addCounter("OtherJoinConjunctEvaluateTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 5000000));
uniqueMetrics.addCounter("OutputBuildColumnTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 8000000));
uniqueMetrics.addCounter("OutputProbeColumnTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 12000000));
uniqueMetrics.addCounter("WhereConjunctEvaluateTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 3000000));
// RuntimeFilter metrics
uniqueMetrics.addCounter("RuntimeFilterBuildTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 5000000));
uniqueMetrics.addCounter("RuntimeFilterNum", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 2));
uniqueMetrics.addCounter("PartialRuntimeMembershipFilterBytes", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 1024 * 1024));
joinProfile.addChild(uniqueMetrics);
// Create pipeline profile structure
RuntimeProfile pipelineProfile = new RuntimeProfile("Pipeline 2");
pipelineProfile.addChild(joinProfile);
mockProfile.getChild("Execution").getChild("Fragment 0").addChild(pipelineProfile);
// Create mock plan with JoinNode
ProfilingExecPlan joinPlan = new ProfilingExecPlan();
Field level = ProfilingExecPlan.class.getDeclaredField("profileLevel");
level.setAccessible(true);
level.setInt(joinPlan, 1);
Field fragmentsField = ProfilingExecPlan.class.getDeclaredField("fragments");
fragmentsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<ProfilingExecPlan.ProfilingFragment> fragments =
(List<ProfilingExecPlan.ProfilingFragment>) fragmentsField.get(joinPlan);
ProfilingExecPlan.ProfilingElement root =
new ProfilingExecPlan.ProfilingElement(2, JoinNode.class);
ProfilingExecPlan.ProfilingElement sink =
new ProfilingExecPlan.ProfilingElement(-1, ResultSink.class);
fragments.add(new ProfilingExecPlan.ProfilingFragment(sink, root));
String result = ExplainAnalyzer.analyze(joinPlan, mockProfile, List.of(2), false);
assertTrue(result.contains("HashTable:"), result);
assertTrue(result.contains("ProbeSide:"), result);
assertTrue(result.contains("RuntimeFilter:"), result);
assertTrue(result.contains("BuildBuckets: "), result);
assertTrue(result.contains("SearchHashTableTime: "), result);
assertTrue(result.contains("RuntimeFilterNum: "), result);
}
@Test
public void testSortOperator() throws NoSuchFieldException, IllegalAccessException {
// Create mock profile for sort operator
RuntimeProfile sortProfile = new RuntimeProfile("SORT (plan_node_id=3)");
sortProfile.addCounter("TotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 120000000));
sortProfile.addCounter("CPUTime", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.TIME_NS, null, 100000000));
sortProfile.addCounter("OutputRows", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 200000));
// Create CommonMetrics profile
RuntimeProfile commonMetrics = new RuntimeProfile("CommonMetrics");
commonMetrics.addCounter("OperatorTotalTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 120000000));
commonMetrics.addCounter("PullRowNum", RuntimeProfile.ROOT_COUNTER, new Counter(TUnit.UNIT, null, 2000000));
commonMetrics.addCounter("OperatorPeakMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 256 * 1024 * 1024));
commonMetrics.addCounter("OperatorAllocatedMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 128 * 1024 * 1024));
sortProfile.addChild(commonMetrics);
// Create UniqueMetrics profile with sort-specific metrics
RuntimeProfile uniqueMetrics = new RuntimeProfile("UniqueMetrics");
// Sort-specific metrics
uniqueMetrics.addCounter("SortTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 80000000));
uniqueMetrics.addCounter("SortKeys", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 2));
uniqueMetrics.addCounter("SortRows", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 2000000));
uniqueMetrics.addCounter("SortMemoryUsage", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 64 * 1024 * 1024));
uniqueMetrics.addCounter("SortSpillBytes", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.BYTES, null, 32 * 1024 * 1024));
uniqueMetrics.addCounter("SortSpillTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 10000000));
uniqueMetrics.addCounter("SortMergeTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 15000000));
uniqueMetrics.addCounter("SortPartitionTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 5000000));
uniqueMetrics.addCounter("SortChunkSize", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 4096));
uniqueMetrics.addCounter("SortChunkCount", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 500));
uniqueMetrics.addCounter("SortCompareTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 20000000));
uniqueMetrics.addCounter("SortCopyTime", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.TIME_NS, null, 10000000));
uniqueMetrics.addCounter("SortLimit", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 100000));
uniqueMetrics.addCounter("SortOffset", RuntimeProfile.ROOT_COUNTER,
new Counter(TUnit.UNIT, null, 0));
sortProfile.addChild(uniqueMetrics);
// Create pipeline profile structure
RuntimeProfile pipelineProfile = new RuntimeProfile("Pipeline 3");
pipelineProfile.addChild(sortProfile);
mockProfile.getChild("Execution").getChild("Fragment 0").addChild(pipelineProfile);
// Create mock plan with SortNode
ProfilingExecPlan sortPlan = new ProfilingExecPlan();
Field level = ProfilingExecPlan.class.getDeclaredField("profileLevel");
level.setAccessible(true);
level.setInt(sortPlan, 1);
Field fragmentsField = ProfilingExecPlan.class.getDeclaredField("fragments");
fragmentsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<ProfilingExecPlan.ProfilingFragment> fragments =
(List<ProfilingExecPlan.ProfilingFragment>) fragmentsField.get(sortPlan);
ProfilingExecPlan.ProfilingElement root =
new ProfilingExecPlan.ProfilingElement(3, SortNode.class);
ProfilingExecPlan.ProfilingElement sink =
new ProfilingExecPlan.ProfilingElement(-1, ResultSink.class);
fragments.add(new ProfilingExecPlan.ProfilingFragment(sink, root));
String result = ExplainAnalyzer.analyze(sortPlan, mockProfile, List.of(3), false);
assertTrue(result.contains("Others:"), result);
assertTrue(result.contains("SortTime: "), result);
assertTrue(result.contains("SortKeys: "), result);
assertTrue(result.contains("SortRows: "), result);
assertTrue(result.contains("SortMemoryUsage: "), result);
assertTrue(result.contains("SortSpillBytes: "), result);
assertTrue(result.contains("SortMergeTime: "), result);
}
}