github-actions[bot] commented on code in PR #66473:
URL: https://github.com/apache/doris/pull/66473#discussion_r3801424905


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -745,52 +750,243 @@ public List<org.apache.paimon.table.source.Split> 
getPaimonSplitFromAPI() throws
             if (PaimonScanParams.isPinnedEmptyScan(resolvedOptions)) {
                 return Collections.emptyList();
             }
-            Optional<Long> fileCreationTime = 
PaimonScanParams.getPinnedFileCreationTime(resolvedOptions);
-            if (fileCreationTime.isPresent()) {
-                if (!(paimonTable instanceof FileStoreTable)) {
-                    throw new UserException("Paimon file-creation OPTIONS 
require a data table.");
+            int[] projectedColumns = new int[0];
+            if 
(!PaimonScanParams.getPinnedFileCreationTime(resolvedOptions).isPresent()) {
+                List<String> fieldNames = 
paimonTable.rowType().getFieldNames();
+                projectedColumns = desc.getSlots().stream().mapToInt(
+                        slot -> getFieldIndex(fieldNames, 
slot.getColumn().getName()))
+                        .toArray();
+                if (Arrays.stream(projectedColumns).anyMatch(index -> index < 
0)) {
+                    throw new UserException("Paimon scan schema does not 
contain all bound Doris columns.");
                 }
-                FileStoreTable fileStoreTable = (FileStoreTable) paimonTable;
-                SnapshotReader snapshotReader = 
fileStoreTable.newSnapshotReader()
-                        .withMode(ScanMode.ALL)
-                        .withSnapshot(Long.parseLong(
-                                
paimonTable.options().get(CoreOptions.SCAN_SNAPSHOT_ID.key())))
-                        .withManifestEntryFilter(entry ->
-                                entry.file().creationTimeEpochMillis() >= 
fileCreationTime.get());
-                preserveBatchScanFilters(fileStoreTable, snapshotReader);
-                if (predicates != null) {
-                    predicates.forEach(snapshotReader::withFilter);
-                }
-                return snapshotReader.read().splits();
-            }
-            List<String> fieldNames = paimonTable.rowType().getFieldNames();
-            int[] projected = desc.getSlots().stream().mapToInt(
-                    slot -> getFieldIndex(fieldNames, 
slot.getColumn().getName()))
-                    .toArray();
-            if (Arrays.stream(projected).anyMatch(index -> index < 0)) {
-                throw new UserException("Paimon scan schema does not contain 
all bound Doris columns.");
             }
-            ReadBuilder readBuilder = paimonTable.newReadBuilder();
-            TableScan scan = readBuilder.withFilter(predicates)
-                    .withProjection(projected)
-                    .newScan();
-            PaimonMetricRegistry registry = new PaimonMetricRegistry();
-            if (scan instanceof InnerTableScan) {
-                scan = ((InnerTableScan) scan).withMetricRegistry(registry);
+            int[] projected = projectedColumns;
+            PaimonSplitTaskCacheKey cacheKey = createPaimonSplitTaskCacheKey(
+                    relationSnapshot, paimonTable, resolvedOptions,
+                    scanParams != null && scanParams.incrementalRead()
+                            ? getIncrReadParams() : Collections.emptyMap(),
+                    projected);
+            if (!canReuseExternalScanTasks()
+                    && !(source.getExternalTable() instanceof 
PaimonSysExternalTable)) {
+                // Reuse is off (or no statement cache): consume the native 
splits directly, so an
+                // opt-out never pays the serialization cost of the cache 
path. System tables keep
+                // the serialized path: their splits must be isolated copies 
for the JNI reader.
+                return planPaimonSplits(paimonTable, resolvedOptions, 
projected);
             }
-            List<org.apache.paimon.table.source.Split> splits = 
scan.plan().splits();
-            PaimonScanMetricsReporter.report(source.getTargetTable(), 
paimonTable.name(), registry);
-            if (!registry.getAllGroups().isEmpty()) {
-                registry.clear();
+            List<PaimonSerializedScanTask> serializedSplits;
+            try {
+                serializedSplits = getOrLoadExternalScanTasks(cacheKey,
+                        remainingBytes -> serializePaimonSplitsWithinLimit(
+                                planPaimonSplits(paimonTable, resolvedOptions, 
projected), remainingBytes),
+                        PaimonScanNode::serializedTaskBytes,
+                        
StatementContext.ExternalScanTaskCache.WeightBudget.PAIMON_SERIALIZED_BYTES,
+                        maxRetainedSerializedTaskBytes, 
maxRetainedSerializedTaskBytes, true);
+            } catch (PaimonTaskCacheLimitException e) {
+                List<org.apache.paimon.table.source.Split> uncachedSplits = 
e.takePlannedSplits();
+                return uncachedSplits == null
+                        ? planPaimonSplits(paimonTable, resolvedOptions, 
projected)
+                        : uncachedSplits;
             }
-            return splits;
+            return serializedSplits.stream()
+                    .map(PaimonSerializedScanTask::deserialize)
+                    .collect(Collectors.toList());
+        } catch (UserException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new UserException("Failed to plan Paimon scan tasks", e);
         } finally {
             if (getSummaryProfile() != null) {
                 
getSummaryProfile().addExternalTableGetFileScanTasksTime(System.currentTimeMillis()
 - startTime);
             }
         }
     }
 
+    private List<org.apache.paimon.table.source.Split> planPaimonSplits(
+            Table paimonTable, Map<String, String> resolvedOptions, int[] 
projected) throws UserException {
+        Optional<Long> fileCreationTime = 
PaimonScanParams.getPinnedFileCreationTime(resolvedOptions);
+        if (fileCreationTime.isPresent()) {
+            if (!(paimonTable instanceof FileStoreTable)) {
+                throw new UserException("Paimon file-creation OPTIONS require 
a data table.");
+            }
+            FileStoreTable fileStoreTable = (FileStoreTable) paimonTable;
+            SnapshotReader snapshotReader = fileStoreTable.newSnapshotReader()
+                    .withMode(ScanMode.ALL)
+                    .withSnapshot(Long.parseLong(
+                            
paimonTable.options().get(CoreOptions.SCAN_SNAPSHOT_ID.key())))
+                    .withManifestEntryFilter(entry ->
+                            entry.file().creationTimeEpochMillis() >= 
fileCreationTime.get());
+            preserveBatchScanFilters(fileStoreTable, snapshotReader);
+            if (predicates != null) {
+                predicates.forEach(snapshotReader::withFilter);
+            }
+            return snapshotReader.read().splits();
+        }
+        ReadBuilder readBuilder = paimonTable.newReadBuilder();
+        TableScan scan = readBuilder.withFilter(predicates)
+                .withProjection(projected)
+                .newScan();
+        PaimonMetricRegistry registry = new PaimonMetricRegistry();
+        if (scan instanceof InnerTableScan) {
+            scan = ((InnerTableScan) scan).withMetricRegistry(registry);
+        }
+        List<org.apache.paimon.table.source.Split> splits = 
scan.plan().splits();
+        PaimonScanMetricsReporter.report(source.getTargetTable(), 
paimonTable.name(), registry);
+        if (!registry.getAllGroups().isEmpty()) {
+            registry.clear();
+        }
+        return splits;
+    }
+
+    private PaimonSplitTaskCacheKey createPaimonSplitTaskCacheKey(
+            Optional<MvccSnapshot> relationSnapshot, Table paimonTable,
+            Map<String, String> resolvedOptions, Map<String, String> 
incrementalOptions,
+            int[] projected) {
+        Long snapshotId = null;
+        Long schemaId = null;
+        if (relationSnapshot.isPresent() && relationSnapshot.get() instanceof 
PaimonMvccSnapshot) {
+            PaimonSnapshot snapshot = ((PaimonMvccSnapshot) 
relationSnapshot.get())
+                    .getSnapshotCacheValue().getSnapshot();
+            snapshotId = snapshot.getSnapshotId();
+            schemaId = snapshot.getSchemaId();
+        }
+        return new PaimonSplitTaskCacheKey(
+                source.getCatalog().getId(),
+                source.getExternalTable().getId(),
+                source.getTargetTable().getId(),
+                snapshotId,
+                schemaId,
+                scanParams == null ? null : scanParams.getParamType(),
+                scanParams == null ? Collections.emptyList() : 
scanParams.getListParams(),
+                resolvedOptions,
+                incrementalOptions,
+                paimonTable.options(),
+                projected,
+                PaimonUtil.encodeObjectToString(predicates));
+    }
+
+    @VisibleForTesting
+    void setMaxRetainedSerializedTaskBytes(long 
maxRetainedSerializedTaskBytes) {
+        this.maxRetainedSerializedTaskBytes = maxRetainedSerializedTaskBytes;
+    }
+
+    private List<PaimonSerializedScanTask> serializePaimonSplitsWithinLimit(
+            List<org.apache.paimon.table.source.Split> splits, long 
maxSerializedBytes) {
+        List<PaimonSerializedScanTask> serializedTasks = new ArrayList<>();
+        long serializedBytes = 0;
+        for (org.apache.paimon.table.source.Split split : splits) {
+            Optional<byte[]> serializedSplit = 
PaimonUtil.serializeObjectWithinLimit(
+                    split, maxSerializedBytes - serializedBytes);
+            if (!serializedSplit.isPresent()) {
+                throw new PaimonTaskCacheLimitException(splits);
+            }
+            PaimonSerializedScanTask task = new 
PaimonSerializedScanTask(serializedSplit.get());
+            serializedBytes += task.serializedSize();
+            serializedTasks.add(task);
+        }
+        return serializedTasks;
+    }
+
+    private static long serializedTaskBytes(List<PaimonSerializedScanTask> 
tasks) {
+        return Math.max(1, 
tasks.stream().mapToLong(PaimonSerializedScanTask::serializedSize).sum());
+    }
+
+    private static final class PaimonSplitTaskCacheKey
+            implements ExternalScanTaskCacheKey<PaimonSerializedScanTask> {
+        private final long catalogId;
+        private final long relationTableId;
+        private final long targetTableId;
+        private final Long snapshotId;
+        private final Long schemaId;
+        private final String scanParamType;
+        private final List<String> listParams;
+        private final Map<String, String> resolvedOptions;
+        private final Map<String, String> incrementalOptions;
+        private final Map<String, String> tableOptions;
+        private final int[] projected;
+        private final String serializedPredicates;
+
+        private PaimonSplitTaskCacheKey(
+                long catalogId, long relationTableId, long targetTableId, Long 
snapshotId, Long schemaId,
+                String scanParamType, List<String> listParams,
+                Map<String, String> resolvedOptions, Map<String, String> 
incrementalOptions,
+                Map<String, String> tableOptions, int[] projected,
+                String serializedPredicates) {
+            this.catalogId = catalogId;
+            this.relationTableId = relationTableId;
+            this.targetTableId = targetTableId;
+            this.snapshotId = snapshotId;
+            this.schemaId = schemaId;
+            this.scanParamType = scanParamType;
+            this.listParams = Collections.unmodifiableList(new 
ArrayList<>(listParams));
+            this.resolvedOptions = Collections.unmodifiableMap(new 
HashMap<>(resolvedOptions));
+            this.incrementalOptions = Collections.unmodifiableMap(new 
HashMap<>(incrementalOptions));
+            this.tableOptions = Collections.unmodifiableMap(new 
HashMap<>(tableOptions));
+            this.projected = Arrays.copyOf(projected, projected.length);
+            this.serializedPredicates = serializedPredicates;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) {
+                return true;
+            }
+            if (!(object instanceof PaimonSplitTaskCacheKey)) {
+                return false;
+            }
+            PaimonSplitTaskCacheKey that = (PaimonSplitTaskCacheKey) object;
+            return catalogId == that.catalogId
+                    && relationTableId == that.relationTableId
+                    && targetTableId == that.targetTableId
+                    && Objects.equals(snapshotId, that.snapshotId)
+                    && Objects.equals(schemaId, that.schemaId)
+                    && Objects.equals(scanParamType, that.scanParamType)
+                    && listParams.equals(that.listParams)
+                    && resolvedOptions.equals(that.resolvedOptions)
+                    && incrementalOptions.equals(that.incrementalOptions)
+                    && tableOptions.equals(that.tableOptions)
+                    && Arrays.equals(projected, that.projected)
+                    && serializedPredicates.equals(that.serializedPredicates);
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * Objects.hash(
+                    catalogId, relationTableId, targetTableId, snapshotId, 
schemaId, scanParamType, listParams,
+                    resolvedOptions, incrementalOptions, tableOptions, 
serializedPredicates)
+                    + Arrays.hashCode(projected);
+        }
+    }
+
+    private static final class PaimonSerializedScanTask {
+        private final byte[] serializedSplit;
+
+        private PaimonSerializedScanTask(byte[] serializedSplit) {
+            this.serializedSplit = serializedSplit;
+        }
+
+        private org.apache.paimon.table.source.Split deserialize() {
+            return PaimonUtil.deserializeObject(Arrays.copyOf(serializedSplit, 
serializedSplit.length));
+        }

Review Comment:
   [P2] Deserialize cached Paimon tasks without cloning the payload
   
   This runs for the owner and every equivalent cache-hit alias, so 
`Arrays.copyOf` allocates another plan-sized set of byte arrays on every 
consumer (up to the 16 MiB retained-task budget in aggregate, and concurrent 
aliases can overlap). `PaimonSerializedScanTask` owns this private final array, 
and Paimon 1.4.2's `InstantiationUtil.deserializeObject` only wraps and reads 
the supplied array via `ByteArrayInputStream`; it does not mutate it. Please 
pass `serializedSplit` directly so reuse does not add this avoidable 
allocation/GC cost.



-- 
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