Jackie-Jiang commented on code in PR #20068:
URL: https://github.com/apache/pinot/pull/20068#discussion_r4406607083


##########
pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java:
##########
@@ -218,35 +219,43 @@ public void setGroupByTrimThreshold(int 
groupByTrimThreshold) {
     _groupByTrimThreshold = groupByTrimThreshold;
   }
 
+  /// Adds one plan node per segment to `planNodes`. With prefetch enabled, a 
segment for which the [FetchPlanner]
+  /// plans a [FetchContext] gets a plan node that acquires and releases its 
buffers around execution, and is added to
+  /// `fetchSegmentContexts` with its context added to `fetchContexts` at the 
same position. A segment for which the
+  /// planner returns null (it needs no fetch) gets the plain plan node, as 
without prefetch.
+  private void addSegmentPlanNodes(List<SegmentContext> segmentContexts, 
QueryContext queryContext,
+      Function<SegmentContext, PlanNode> segmentPlanNodeMaker, List<PlanNode> 
planNodes,
+      List<SegmentContext> fetchSegmentContexts, List<FetchContext> 
fetchContexts) {
+    boolean enablePrefetch = queryContext.isEnablePrefetch();
+    for (SegmentContext segmentContext : segmentContexts) {
+      FetchContext fetchContext =
+          enablePrefetch ? 
_fetchPlanner.planFetchForProcessing(segmentContext.getIndexSegment(), 
queryContext) : null;
+      if (fetchContext != null) {
+        fetchSegmentContexts.add(segmentContext);
+        fetchContexts.add(fetchContext);
+        planNodes.add(new 
AcquireReleaseColumnsSegmentPlanNode(segmentPlanNodeMaker.apply(segmentContext),
+            segmentContext, fetchContext));
+      } else {
+        planNodes.add(segmentPlanNodeMaker.apply(segmentContext));
+      }
+    }
+  }
+
   @Override
   public Plan makeInstancePlan(List<SegmentContext> segmentContexts, 
QueryContext queryContext,
       ExecutorService executorService) {
     applyQueryOptions(queryContext);
     boundGroupingSetsBaseAggregation(segmentContexts, queryContext);
 
-    int numSegments = segmentContexts.size();
-    List<PlanNode> planNodes = new ArrayList<>(numSegments);
-    List<FetchContext> fetchContexts;
-    if (queryContext.isEnablePrefetch()) {
-      fetchContexts = new ArrayList<>(numSegments);
-      for (SegmentContext segmentContext : segmentContexts) {
-        FetchContext fetchContext =
-            
_fetchPlanner.planFetchForProcessing(segmentContext.getIndexSegment(), 
queryContext);
-        fetchContexts.add(fetchContext);
-        planNodes.add(
-            new 
AcquireReleaseColumnsSegmentPlanNode(makeSegmentPlanNode(segmentContext, 
queryContext), segmentContext,
-                fetchContext));
-      }
-    } else {
-      fetchContexts = List.of();
-      for (SegmentContext segmentContext : segmentContexts) {
-        planNodes.add(makeSegmentPlanNode(segmentContext, queryContext));
-      }
-    }
+    List<PlanNode> planNodes = new ArrayList<>(segmentContexts.size());
+    List<SegmentContext> fetchSegmentContexts = new ArrayList<>();
+    List<FetchContext> fetchContexts = new ArrayList<>();

Review Comment:
   Minor allocation cleanup: both fetch lists are now allocated even when 
prefetch is disabled, and the enabled path loses the previous `numSegments` 
capacity hint. Use empty lists when prefetch is disabled and pre-size the 
mutable lists when enabled to avoid repeated growth for queries fetching many 
segments. Please apply the same cleanup to `makeStreamingInstancePlan()`. This 
is a bounded allocation improvement, not a demonstrated material performance 
regression.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/prefetch/FetchPlanner.java:
##########
@@ -27,21 +28,27 @@
 /// read from some slow storage backend, to reduce the query latency. Although 
the plan methods take in an indexSegment
 /// object, it should mainly access the segment metadata like what types of 
index it has, as the index data may not be
 /// available, but leaving this to the implementations to check and handle per 
their own need.
+///
+/// A planner can return null for a segment that needs no fetch, e.g. one 
whose data is all local, so its fetch calls
+/// would be no-ops. The caller then skips the fetch lifecycle (prefetch, 
acquire, release) for that segment entirely.
+/// A non-null context, even an empty one, keeps the lifecycle.

Review Comment:
   Minor: this promises that any non-null context, including an empty one, 
preserves the fetch lifecycle, but `BloomFilterSegmentPruner.prefetch()` still 
skips empty contexts via `fetchContext != null && !fetchContext.isEmpty()`. 
Please scope this guarantee to `planFetchForProcessing()` and document that 
pruning skips both null and empty contexts. The existing pruning behavior can 
remain unchanged.



##########
pinot-core/src/test/java/org/apache/pinot/core/plan/maker/InstancePlanMakerPrefetchTest.java:
##########
@@ -0,0 +1,227 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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 org.apache.pinot.core.plan.maker;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.common.datatable.DataTable;
+import org.apache.pinot.core.operator.blocks.InstanceResponseBlock;
+import org.apache.pinot.core.query.prefetch.DefaultFetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlannerRegistry;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import 
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
+import 
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
+import 
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.spi.FetchContext;
+import org.apache.pinot.segment.spi.ImmutableSegment;
+import org.apache.pinot.segment.spi.IndexSegment;
+import org.apache.pinot.segment.spi.SegmentContext;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.query.QueryThreadContext;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// With prefetch enabled, [InstancePlanMakerImplV2] runs the fetch lifecycle 
only for segments for which the
+/// [FetchPlanner] plans a fetch context. A segment for which the planner 
returns null (it needs no fetch) is planned
+/// and executed as without prefetch.
+public class InstancePlanMakerPrefetchTest {
+  private static final String TABLE_NAME = "testTable";
+  private static final String SEGMENT_NAME = "testSegment";
+  private static final int NUM_DOCS = 100;
+  // Every tenth document matches
+  private static final String QUERY = "SELECT COUNT(*), SUM(m) FROM testTable 
WHERE d = 'v3'";
+  private static final long MATCHES_PER_SEGMENT = NUM_DOCS / 10;
+  private static final double SUM_PER_SEGMENT = 3 + 13 + 23 + 33 + 43 + 53 + 
63 + 73 + 83 + 93;
+
+  private File _tempDir;
+  private ImmutableSegment _segment;
+  private ExecutorService _executorService;
+
+  @BeforeClass
+  public void setUp()
+      throws Exception {
+    _tempDir = 
Files.createTempDirectory("instance-plan-maker-prefetch-test").toFile();
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addSingleValueDimension("d", DataType.STRING)
+        .addMetric("m", DataType.LONG)
+        .build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+    List<GenericRow> rows = new ArrayList<>(NUM_DOCS);
+    for (int i = 0; i < NUM_DOCS; i++) {
+      GenericRow row = new GenericRow();
+      row.putValue("d", "v" + (i % 10));
+      row.putValue("m", (long) i);
+      rows.add(row);
+    }
+    SegmentGeneratorConfig config = new SegmentGeneratorConfig(tableConfig, 
schema);
+    config.setTableName(TABLE_NAME);
+    config.setSegmentName(SEGMENT_NAME);
+    config.setOutDir(_tempDir.getAbsolutePath());
+    SegmentIndexCreationDriverImpl driver = new 
SegmentIndexCreationDriverImpl();
+    driver.init(config, new GenericRowRecordReader(rows));
+    driver.build();
+    _segment =
+        ImmutableSegmentLoader.load(new File(_tempDir, SEGMENT_NAME), new 
IndexLoadingConfig(tableConfig, schema));
+    _executorService = 
QueryThreadContext.contextAwareExecutorService(Executors.newFixedThreadPool(2));
+  }
+
+  @AfterClass
+  public void tearDown() {
+    _executorService.shutdownNow();
+    _segment.destroy();
+    FileUtils.deleteQuietly(_tempDir);
+  }
+
+  @Test
+  public void testPrefetchRunsFetchLifecycleOnlyForPlannedSegments()
+      throws Exception {
+    IndexSegment local = spy(_segment);
+    IndexSegment remote = spy(_segment);
+
+    assertResult(execute(plannerSkipping(Set.of(local)), List.of(local, 
remote), true), 2);
+
+    ArgumentCaptor<FetchContext> fetchContext = 
ArgumentCaptor.forClass(FetchContext.class);
+    verify(remote).prefetch(fetchContext.capture());
+    verify(remote).acquire(fetchContext.getValue());
+    verify(remote, atLeastOnce()).release(fetchContext.getValue());
+    
assertTrue(fetchContext.getValue().getColumnToIndexList().containsKey("d"));
+    verifyNoFetch(local);
+  }
+
+  @Test
+  public void testPrefetchWithoutPlannedSegmentsRunsNoFetchLifecycle()
+      throws Exception {
+    IndexSegment first = spy(_segment);
+    IndexSegment second = spy(_segment);
+
+    assertResult(execute(plannerSkipping(Set.of(first, second)), 
List.of(first, second), true), 2);
+
+    verifyNoFetch(first);
+    verifyNoFetch(second);
+  }
+
+  @Test
+  public void testDisabledPrefetchRunsNoFetchLifecycle()
+      throws Exception {
+    IndexSegment segment = spy(_segment);
+
+    assertResult(execute(plannerSkipping(Set.of()), List.of(segment), false), 
1);
+
+    verifyNoFetch(segment);
+  }
+
+  /// The default planner never returns null, so every segment keeps the fetch 
lifecycle as before.
+  @Test
+  public void testDefaultPlannerRunsFetchLifecycleForEverySegment()
+      throws Exception {
+    IndexSegment first = spy(_segment);
+    IndexSegment second = spy(_segment);
+
+    assertResult(execute(new DefaultFetchPlanner(), List.of(first, second), 
true), 2);
+
+    for (IndexSegment segment : List.of(first, second)) {
+      verify(segment).prefetch(any());
+      verify(segment).acquire(any());
+      verify(segment, atLeastOnce()).release(any());
+    }
+  }
+
+  /// A planner that plans with the default planner, except for the given 
segments, which need no fetch.
+  private static FetchPlanner plannerSkipping(Set<IndexSegment> 
segmentsWithoutFetch) {
+    FetchPlanner defaultPlanner = new DefaultFetchPlanner();
+    return new FetchPlanner() {
+      @Override
+      public FetchContext planFetchForPruning(IndexSegment indexSegment, 
QueryContext queryContext) {
+        return segmentsWithoutFetch.contains(indexSegment) ? null
+            : defaultPlanner.planFetchForPruning(indexSegment, queryContext);
+      }
+
+      @Override
+      public FetchContext planFetchForProcessing(IndexSegment indexSegment, 
QueryContext queryContext) {
+        return segmentsWithoutFetch.contains(indexSegment) ? null
+            : defaultPlanner.planFetchForProcessing(indexSegment, 
queryContext);
+      }
+    };
+  }
+
+  private DataTable execute(FetchPlanner fetchPlanner, List<IndexSegment> 
segments, boolean enablePrefetch)
+      throws Exception {
+    QueryContext queryContext = 
QueryContextConverterUtils.getQueryContext(QUERY);
+    queryContext.setEndTimeMs(System.currentTimeMillis() + 60_000);
+    queryContext.setEnablePrefetch(enablePrefetch);
+    List<SegmentContext> segmentContexts = new ArrayList<>(segments.size());
+    for (IndexSegment segment : segments) {
+      segmentContexts.add(new SegmentContext(segment));
+    }
+    try (QueryThreadContext ignored = QueryThreadContext.openForSseTest()) {
+      InstanceResponseBlock responseBlock = planMakerWith(fetchPlanner)
+          .makeInstancePlan(segmentContexts, queryContext, 
_executorService).execute();

Review Comment:
   Optional coverage improvement: all tests in this class execute a 
non-streaming aggregation through `makeInstancePlan()`. Please retain a mixed 
local/remote streaming-selection case using `makeStreamingInstancePlan()` and a 
`ResultsBlockStreamer`, asserting the streamed rows and selective 
prefetch/acquire/release calls. Streaming selection exercises the separate 
start/stop and multi-block lifecycle, so this would protect the streaming 
change as well.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to