This is an automated email from the ASF dual-hosted git repository.

Caideyipi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new a51258bbb97 [Performance] Reduce aligned flush duplicate timestamp 
overhead
a51258bbb97 is described below

commit a51258bbb979b0601e1688fb8f88adc65d518c93
Author: Caideyipi <[email protected]>
AuthorDate: Mon Sep 28 15:44:39 2026 +0800

    [Performance] Reduce aligned flush duplicate timestamp overhead
---
 .../memtable/AlignedWritableMemChunk.java          | 119 ++++++-----
 ...gnedFlushDuplicateTimeCheckPerformanceTest.java | 238 +++++++++++++++++++++
 .../dataregion/memtable/MemTableFlushTaskTest.java |  83 +++++++
 3 files changed, 386 insertions(+), 54 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
index e96baf88b3a..ebdc024aac8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunk.java
@@ -709,41 +709,41 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
           TypeServices.StorageEngine.ALIGNED_TV_LIST_CHUNK_WRITER_SERVICE.call(
               Type.fromTsDataType(dataType)));
     }
-    Pair<Long, Integer>[] lastValidPointIndexForTimeDupCheck = new 
Pair[dataTypes.size()];
+    long[] lastValidPointTimeForTimeDupCheck =
+        timeDuplicateInfo == null ? null : new long[dataTypes.size()];
+    int[] lastValidPointIndexForTimeDupCheck =
+        timeDuplicateInfo == null ? null : new int[dataTypes.size()];
+    if (lastValidPointIndexForTimeDupCheck != null) {
+      Arrays.fill(lastValidPointIndexForTimeDupCheck, -1);
+    }
     for (List<Integer> pageRange : chunkRange) {
       AlignedChunkWriterImpl alignedChunkWriter =
           new AlignedChunkWriterImpl(schemaList, encryptParameter);
       for (int pageNum = 0; pageNum < pageRange.size() / 2; pageNum += 1) {
         for (int columnIndex = 0; columnIndex < dataTypes.size(); 
columnIndex++) {
-          // Pair of Time and Index
-          if (Objects.nonNull(timeDuplicateInfo)
-              && lastValidPointIndexForTimeDupCheck[columnIndex] == null) {
-            lastValidPointIndexForTimeDupCheck[columnIndex] = new 
Pair<>(Long.MIN_VALUE, null);
-          }
           TypeServices.AlignedTVListChunkWriter valueWriter = 
valueWriters.get(columnIndex);
           ValueChunkWriter valueChunkWriter =
               alignedChunkWriter.getValueChunkWriterByIndex(columnIndex);
           for (int sortedRowIndex = pageRange.get(pageNum * 2);
               sortedRowIndex <= pageRange.get(pageNum * 2 + 1);
               sortedRowIndex++) {
+            int valueIndex = 
alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
             // skip empty row
-            if (allValueColDeletedMap != null
-                && allValueColDeletedMap.isMarked(
-                    alignedWorkingListForFlush.getValueIndex(sortedRowIndex))) 
{
+            if (allValueColDeletedMap != null && 
allValueColDeletedMap.isMarked(valueIndex)) {
               continue;
             }
             // Keep value pages aligned with the time page when an entire 
timestamp is deleted.
-            if (alignedWorkingListForFlush.isTimeDeleted(sortedRowIndex)) {
+            if (alignedWorkingListForFlush.isTimeDeleted(valueIndex, false)) {
               continue;
             }
             // skip time duplicated rows
             long time = alignedWorkingListForFlush.getTime(sortedRowIndex);
-            if (Objects.nonNull(timeDuplicateInfo)) {
-              if (!alignedWorkingListForFlush.isNullValue(
-                  alignedWorkingListForFlush.getValueIndex(sortedRowIndex), 
columnIndex)) {
-                lastValidPointIndexForTimeDupCheck[columnIndex].left = time;
-                lastValidPointIndexForTimeDupCheck[columnIndex].right =
-                    alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
+            boolean currentValueIsNull = false;
+            if (timeDuplicateInfo != null) {
+              currentValueIsNull = 
alignedWorkingListForFlush.isNullValue(valueIndex, columnIndex);
+              if (!currentValueIsNull) {
+                lastValidPointTimeForTimeDupCheck[columnIndex] = time;
+                lastValidPointIndexForTimeDupCheck[columnIndex] = valueIndex;
               }
               if (timeDuplicateInfo[sortedRowIndex]) {
                 continue;
@@ -753,21 +753,25 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
             // The part of code solves the following problem:
             // Time: 1,2,2,3
             // Value: 1,2,null,null
-            // When rowIndex:1, pair(min,null), timeDuplicateInfo:false, 
write(T:1,V:1)
-            // When rowIndex:2, pair(2,2), timeDuplicateInfo:true, skip 
writing value
-            // When rowIndex:3, pair(2,2), timeDuplicateInfo:false, 
T:2==pair.left:2, write(T:2,V:2)
-            // When rowIndex:4, pair(2,2), timeDuplicateInfo:false, 
T:3!=pair.left:2,
+            // When rowIndex:1, no valid duplicate value, 
timeDuplicateInfo:false, write(T:1,V:1)
+            // When rowIndex:2, last valid point=(2,2), 
timeDuplicateInfo:true, skip writing value
+            // When rowIndex:3, last valid point=(2,2), T:2 matches, 
write(T:2,V:2)
+            // When rowIndex:4, last valid point=(2,2), T:3 doesn't match,
             // write(T:3,V:null)
 
             int originRowIndex;
-            if 
(Objects.nonNull(lastValidPointIndexForTimeDupCheck[columnIndex])
-                && (time == 
lastValidPointIndexForTimeDupCheck[columnIndex].left)) {
-              originRowIndex = 
lastValidPointIndexForTimeDupCheck[columnIndex].right;
+            if (timeDuplicateInfo != null
+                && lastValidPointIndexForTimeDupCheck[columnIndex] >= 0
+                && time == lastValidPointTimeForTimeDupCheck[columnIndex]) {
+              originRowIndex = lastValidPointIndexForTimeDupCheck[columnIndex];
             } else {
-              originRowIndex = 
alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
+              originRowIndex = valueIndex;
             }
 
-            boolean isNull = 
alignedWorkingListForFlush.isNullValue(originRowIndex, columnIndex);
+            boolean isNull =
+                timeDuplicateInfo == null
+                    ? alignedWorkingListForFlush.isNullValue(originRowIndex, 
columnIndex)
+                    : originRowIndex == valueIndex && currentValueIsNull;
             valueWriter.write(
                 valueChunkWriter,
                 time,
@@ -947,18 +951,19 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
       List<IMeasurementSchema> activeSchemaList) {
     AlignedTVList alignedWorkingListForFlush = (AlignedTVList) 
workingListForFlush;
     List<Integer> columnIndexList = buildColumnIndexList(activeSchemaList);
-    Pair<Long, Integer>[] lastValidPointIndexForTimeDupCheck = new 
Pair[activeSchemaList.size()];
+    long[] lastValidPointTimeForTimeDupCheck =
+        timeDuplicateInfo == null ? null : new long[activeSchemaList.size()];
+    int[] lastValidPointIndexForTimeDupCheck =
+        timeDuplicateInfo == null ? null : new int[activeSchemaList.size()];
+    if (lastValidPointIndexForTimeDupCheck != null) {
+      Arrays.fill(lastValidPointIndexForTimeDupCheck, -1);
+    }
     for (List<Integer> pageRange : chunkRange) {
       AlignedChunkWriterImpl alignedChunkWriter =
           new AlignedChunkWriterImpl(activeSchemaList, encryptParameter);
       for (int pageNum = 0; pageNum < pageRange.size() / 2; pageNum += 1) {
         for (int columnIndex = 0; columnIndex < activeSchemaList.size(); 
columnIndex++) {
           int tvListColumnIndex = columnIndexList.get(columnIndex);
-          // Pair of Time and Index
-          if (Objects.nonNull(timeDuplicateInfo)
-              && lastValidPointIndexForTimeDupCheck[columnIndex] == null) {
-            lastValidPointIndexForTimeDupCheck[columnIndex] = new 
Pair<>(Long.MIN_VALUE, null);
-          }
           TSDataType tsDataType = activeSchemaList.get(columnIndex).getType();
           TypeServices.AlignedTVListColumnWriter columnWriter =
               
TypeServices.StorageEngine.ALIGNED_TV_LIST_COLUMN_WRITER_SERVICE.call(
@@ -966,27 +971,27 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
           for (int sortedRowIndex = pageRange.get(pageNum * 2);
               sortedRowIndex <= pageRange.get(pageNum * 2 + 1);
               sortedRowIndex++) {
+            int valueIndex = 
alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
             // skip empty row
-            if (allValueColDeletedMap != null
-                && allValueColDeletedMap.isMarked(
-                    alignedWorkingListForFlush.getValueIndex(sortedRowIndex))) 
{
+            if (allValueColDeletedMap != null && 
allValueColDeletedMap.isMarked(valueIndex)) {
               continue;
             }
             // Keep value pages aligned with the time page when an entire 
timestamp is deleted.
-            if (alignedWorkingListForFlush.isTimeDeleted(sortedRowIndex)) {
+            if (alignedWorkingListForFlush.isTimeDeleted(valueIndex, false)) {
               continue;
             }
             // skip time duplicated rows
             long time = alignedWorkingListForFlush.getTime(sortedRowIndex);
-            if (Objects.nonNull(timeDuplicateInfo)) {
-              if (tvListColumnIndex >= 0
-                  && !alignedWorkingListForFlush.isNullValue(
-                      alignedWorkingListForFlush.getValueIndex(sortedRowIndex),
-                      tvListColumnIndex)) {
-                lastValidPointIndexForTimeDupCheck[columnIndex].left = time;
-                lastValidPointIndexForTimeDupCheck[columnIndex].right =
-                    alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
+            boolean currentValueIsNull = true;
+            if (timeDuplicateInfo != null && tvListColumnIndex >= 0) {
+              currentValueIsNull =
+                  alignedWorkingListForFlush.isNullValue(valueIndex, 
tvListColumnIndex);
+              if (!currentValueIsNull) {
+                lastValidPointTimeForTimeDupCheck[columnIndex] = time;
+                lastValidPointIndexForTimeDupCheck[columnIndex] = valueIndex;
               }
+            }
+            if (timeDuplicateInfo != null) {
               if (timeDuplicateInfo[sortedRowIndex]) {
                 continue;
               }
@@ -995,24 +1000,30 @@ public class AlignedWritableMemChunk extends 
AbstractWritableMemChunk {
             // The part of code solves the following problem:
             // Time: 1,2,2,3
             // Value: 1,2,null,null
-            // When rowIndex:1, pair(min,null), timeDuplicateInfo:false, 
write(T:1,V:1)
-            // When rowIndex:2, pair(2,2), timeDuplicateInfo:true, skip 
writing value
-            // When rowIndex:3, pair(2,2), timeDuplicateInfo:false, 
T:2==pair.left:2, write(T:2,V:2)
-            // When rowIndex:4, pair(2,2), timeDuplicateInfo:false, 
T:3!=pair.left:2,
+            // When rowIndex:1, no valid duplicate value, 
timeDuplicateInfo:false, write(T:1,V:1)
+            // When rowIndex:2, last valid point=(2,2), 
timeDuplicateInfo:true, skip writing value
+            // When rowIndex:3, last valid point=(2,2), T:2 matches, 
write(T:2,V:2)
+            // When rowIndex:4, last valid point=(2,2), T:3 doesn't match,
             // write(T:3,V:null)
 
             int originRowIndex;
             if (tvListColumnIndex >= 0
-                && 
Objects.nonNull(lastValidPointIndexForTimeDupCheck[columnIndex])
-                && (time == 
lastValidPointIndexForTimeDupCheck[columnIndex].left)) {
-              originRowIndex = 
lastValidPointIndexForTimeDupCheck[columnIndex].right;
+                && timeDuplicateInfo != null
+                && lastValidPointIndexForTimeDupCheck[columnIndex] >= 0
+                && time == lastValidPointTimeForTimeDupCheck[columnIndex]) {
+              originRowIndex = lastValidPointIndexForTimeDupCheck[columnIndex];
             } else {
-              originRowIndex = 
alignedWorkingListForFlush.getValueIndex(sortedRowIndex);
+              originRowIndex = valueIndex;
             }
 
-            boolean isNull =
-                tvListColumnIndex < 0
-                    || alignedWorkingListForFlush.isNullValue(originRowIndex, 
tvListColumnIndex);
+            boolean isNull;
+            if (tvListColumnIndex < 0) {
+              isNull = true;
+            } else if (timeDuplicateInfo == null) {
+              isNull = alignedWorkingListForFlush.isNullValue(originRowIndex, 
tvListColumnIndex);
+            } else {
+              isNull = originRowIndex == valueIndex && currentValueIsNull;
+            }
             columnWriter.write(
                 alignedChunkWriter,
                 time,
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedFlushDuplicateTimeCheckPerformanceTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedFlushDuplicateTimeCheckPerformanceTest.java
new file mode 100644
index 00000000000..1f396efb6ff
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedFlushDuplicateTimeCheckPerformanceTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.iotdb.db.storageengine.dataregion.memtable;
+
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Measurement;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Summary;
+import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.utils.Pair;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class AlignedFlushDuplicateTimeCheckPerformanceTest {
+
+  private static final String ENABLED_PROPERTY = 
"iotdb.aligned.flush.duplicate-check.perf.enabled";
+  private static final String ROUNDS_PROPERTY = 
"iotdb.aligned.flush.duplicate-check.perf.rounds";
+  private static final String SCANS_PROPERTY = 
"iotdb.aligned.flush.duplicate-check.perf.scans";
+  private static final int ROW_COUNT = 16_384;
+  private static final int COLUMN_COUNT = 8;
+  private static final int WARMUP_ROUNDS = 3;
+
+  private static volatile long benchmarkBlackhole;
+
+  @Test
+  public void duplicateTimeCheckBenchmark() {
+    Assume.assumeTrue(
+        String.format(
+            Locale.ROOT,
+            "Manual performance UT. Enable with -D%s=true; optionally tune 
-D%s and -D%s.",
+            ENABLED_PROPERTY,
+            ROUNDS_PROPERTY,
+            SCANS_PROPERTY),
+        Boolean.getBoolean(ENABLED_PROPERTY));
+    Assume.assumeTrue(
+        "Current-thread CPU time and allocation metrics are required.",
+        ManualPerformanceTestUtils.enableThreadMetrics());
+
+    int rounds = Integer.getInteger(ROUNDS_PROPERTY, 5);
+    int scans = Integer.getInteger(SCANS_PROPERTY, 128);
+    Assert.assertTrue(rounds > 0);
+    Assert.assertTrue(scans > 0);
+
+    BenchmarkData data = createBenchmarkData();
+    long legacyResult = runLegacy(data);
+    long optimizedResult = runOptimized(data);
+    Assert.assertEquals(legacyResult, optimizedResult);
+
+    for (int i = 0; i < WARMUP_ROUNDS; i++) {
+      runLegacy(data);
+      runOptimized(data);
+    }
+
+    Measurement[] legacyMeasurements = new Measurement[rounds];
+    Measurement[] optimizedMeasurements = new Measurement[rounds];
+    for (int i = 0; i < rounds; i++) {
+      if ((i & 1) == 0) {
+        legacyMeasurements[i] = measureLegacy(data, scans);
+        optimizedMeasurements[i] = measureOptimized(data, scans);
+      } else {
+        optimizedMeasurements[i] = measureOptimized(data, scans);
+        legacyMeasurements[i] = measureLegacy(data, scans);
+      }
+    }
+
+    Summary legacySummary = 
ManualPerformanceTestUtils.summarize(legacyMeasurements, scans);
+    Summary optimizedSummary = 
ManualPerformanceTestUtils.summarize(optimizedMeasurements, scans);
+    printResult(rounds, scans, legacySummary, optimizedSummary);
+  }
+
+  private static Measurement measureLegacy(BenchmarkData data, int scans) {
+    return ManualPerformanceTestUtils.measure(scans, () -> runLegacy(data));
+  }
+
+  private static Measurement measureOptimized(BenchmarkData data, int scans) {
+    return ManualPerformanceTestUtils.measure(scans, () -> runOptimized(data));
+  }
+
+  private static long runLegacy(BenchmarkData data) {
+    long nullCount = 0;
+    Pair<Long, Integer>[] lastValidPointForTimeDupCheck = new 
Pair[COLUMN_COUNT];
+    for (int columnIndex = 0; columnIndex < COLUMN_COUNT; columnIndex++) {
+      for (int rowIndex = 0; rowIndex < ROW_COUNT; rowIndex++) {
+        if (lastValidPointForTimeDupCheck[columnIndex] == null) {
+          lastValidPointForTimeDupCheck[columnIndex] = new 
Pair<>(Long.MIN_VALUE, null);
+        }
+        int valueIndex = data.list.getValueIndex(rowIndex);
+        long time = data.list.getTime(rowIndex);
+        if (!data.list.isNullValue(valueIndex, columnIndex)) {
+          lastValidPointForTimeDupCheck[columnIndex].left = time;
+          lastValidPointForTimeDupCheck[columnIndex].right = valueIndex;
+        }
+        if (data.timeDuplicateInfo[rowIndex]) {
+          continue;
+        }
+
+        int originRowIndex;
+        if (time == lastValidPointForTimeDupCheck[columnIndex].left) {
+          originRowIndex = lastValidPointForTimeDupCheck[columnIndex].right;
+        } else {
+          originRowIndex = valueIndex;
+        }
+        if (data.list.isNullValue(originRowIndex, columnIndex)) {
+          nullCount++;
+        }
+      }
+    }
+    benchmarkBlackhole = nullCount;
+    return nullCount;
+  }
+
+  private static long runOptimized(BenchmarkData data) {
+    long nullCount = 0;
+    long[] lastValidPointTimeForTimeDupCheck = new long[COLUMN_COUNT];
+    int[] lastValidPointIndexForTimeDupCheck = new int[COLUMN_COUNT];
+    Arrays.fill(lastValidPointIndexForTimeDupCheck, -1);
+    for (int columnIndex = 0; columnIndex < COLUMN_COUNT; columnIndex++) {
+      for (int rowIndex = 0; rowIndex < ROW_COUNT; rowIndex++) {
+        int valueIndex = data.list.getValueIndex(rowIndex);
+        long time = data.list.getTime(rowIndex);
+        boolean currentValueIsNull = data.list.isNullValue(valueIndex, 
columnIndex);
+        if (!currentValueIsNull) {
+          lastValidPointTimeForTimeDupCheck[columnIndex] = time;
+          lastValidPointIndexForTimeDupCheck[columnIndex] = valueIndex;
+        }
+        if (data.timeDuplicateInfo[rowIndex]) {
+          continue;
+        }
+
+        int originRowIndex;
+        if (lastValidPointIndexForTimeDupCheck[columnIndex] >= 0
+            && time == lastValidPointTimeForTimeDupCheck[columnIndex]) {
+          originRowIndex = lastValidPointIndexForTimeDupCheck[columnIndex];
+        } else {
+          originRowIndex = valueIndex;
+        }
+        boolean isNull = originRowIndex == valueIndex && currentValueIsNull;
+        if (isNull) {
+          nullCount++;
+        }
+      }
+    }
+    benchmarkBlackhole = nullCount;
+    return nullCount;
+  }
+
+  private static BenchmarkData createBenchmarkData() {
+    List<TSDataType> dataTypes = new ArrayList<>(COLUMN_COUNT);
+    for (int columnIndex = 0; columnIndex < COLUMN_COUNT; columnIndex++) {
+      dataTypes.add(TSDataType.INT64);
+    }
+    AlignedTVList list = AlignedTVList.newAlignedList(dataTypes);
+    for (int rowIndex = 0; rowIndex < ROW_COUNT; rowIndex++) {
+      Object[] values = new Object[COLUMN_COUNT];
+      for (int columnIndex = 0; columnIndex < COLUMN_COUNT; columnIndex++) {
+        if ((rowIndex + columnIndex) % 4 != 0) {
+          values[columnIndex] = (long) rowIndex + columnIndex;
+        }
+      }
+      list.putAlignedValue(rowIndex / 4, values);
+    }
+    list.sort();
+
+    boolean[] timeDuplicateInfo = new boolean[ROW_COUNT];
+    for (int rowIndex = 0; rowIndex < ROW_COUNT - 1; rowIndex++) {
+      timeDuplicateInfo[rowIndex] = list.getTime(rowIndex) == 
list.getTime(rowIndex + 1);
+    }
+    return new BenchmarkData(list, timeDuplicateInfo);
+  }
+
+  private static void printResult(
+      int rounds, int scans, Summary legacySummary, Summary optimizedSummary) {
+    System.out.printf(
+        Locale.ROOT,
+        "Aligned flush duplicate-time check benchmark: rows=%d, columns=%d, 
value checks/scan=%d, scans/round=%d, rounds=%d%n",
+        ROW_COUNT,
+        COLUMN_COUNT,
+        ROW_COUNT * COLUMN_COUNT,
+        scans,
+        rounds);
+    printSummary("legacy", legacySummary);
+    printSummary("optimized", optimizedSummary);
+    System.out.printf(
+        Locale.ROOT,
+        "  optimized/legacy CPU ratio=%.2f%%, allocation ratio=%.2f%%%n",
+        percentage(
+            optimizedSummary.getCpuNanosPerOperation(), 
legacySummary.getCpuNanosPerOperation()),
+        percentage(
+            optimizedSummary.getAllocatedBytesPerOperation(),
+            legacySummary.getAllocatedBytesPerOperation()));
+  }
+
+  private static void printSummary(String label, Summary summary) {
+    System.out.printf(
+        Locale.ROOT,
+        "  %-10s CPU=%.3f ms/scan, allocated=%.2f bytes/scan%n",
+        label,
+        summary.getCpuNanosPerOperation() / 1_000_000.0,
+        summary.getAllocatedBytesPerOperation());
+  }
+
+  private static double percentage(double numerator, double denominator) {
+    return denominator == 0 ? 0 : numerator * 100.0 / denominator;
+  }
+
+  private static final class BenchmarkData {
+    private final AlignedTVList list;
+    private final boolean[] timeDuplicateInfo;
+
+    private BenchmarkData(AlignedTVList list, boolean[] timeDuplicateInfo) {
+      this.list = list;
+      this.timeDuplicateInfo = timeDuplicateInfo;
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/MemTableFlushTaskTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/MemTableFlushTaskTest.java
index ad1cbc219de..afe0384839b 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/MemTableFlushTaskTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/MemTableFlushTaskTest.java
@@ -212,6 +212,89 @@ public class MemTableFlushTaskTest {
     assertFalse(ioTaskQueue.isEmpty());
   }
 
+  @Test
+  public void testAlignedFlushMergesDuplicateTimestampsUsingLastNonNullValue() 
throws Exception {
+    checkAlignedFlushMergesDuplicateTimestamps(false);
+  }
+
+  @Test
+  public void 
testAlignedFlushMergesDuplicateTimestampsAfterMeasurementDeletion() throws 
Exception {
+    checkAlignedFlushMergesDuplicateTimestamps(true);
+  }
+
+  private void checkAlignedFlushMergesDuplicateTimestamps(boolean 
removeSecondMeasurement)
+      throws Exception {
+    List<IMeasurementSchema> schemas =
+        Arrays.asList(
+            new MeasurementSchema("s0", TSDataType.INT64, TSEncoding.PLAIN),
+            new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN));
+    AlignedWritableMemChunk memChunk = new AlignedWritableMemChunk(schemas, 
true);
+    String alignedFilePath =
+        TestConstant.OUTPUT_DATA_DIR.concat(
+            "duplicateAlignedRows-" + removeSecondMeasurement + ".tsfile");
+
+    try {
+      memChunk.putAlignedRow(1, new Object[] {10L, 100L});
+      memChunk.putAlignedRow(2, new Object[] {20L, null});
+      memChunk.putAlignedRow(2, new Object[] {null, 200L});
+      memChunk.putAlignedRow(2, new Object[] {null, null});
+      memChunk.putAlignedRow(3, new Object[] {30L, null});
+      if (removeSecondMeasurement) {
+        memChunk.removeColumn("s1");
+      }
+      memChunk.sortTvListForFlush();
+
+      BlockingQueue<Object> ioTaskQueue = new LinkedBlockingQueue<>();
+      memChunk.encodeWorkingAlignedTVList(ioTaskQueue, 100, 100);
+      try (TsFileIOWriter alignedWriter = new TsFileIOWriter(new 
File(alignedFilePath))) {
+        
alignedWriter.startChunkGroup(IDeviceID.Factory.DEFAULT_FACTORY.create("root.d"));
+        Object task;
+        while ((task = ioTaskQueue.poll()) != null) {
+          if (task instanceof IChunkWriter chunkWriter) {
+            chunkWriter.writeToFileWriter(alignedWriter);
+          }
+        }
+        alignedWriter.endChunkGroup();
+        alignedWriter.endFile();
+      }
+
+      try (TsFileSequenceReader sequenceReader = new 
TsFileSequenceReader(alignedFilePath);
+          TsFileReader fileReader = new TsFileReader(sequenceReader)) {
+        List<Path> paths = new ArrayList<>();
+        paths.add(new Path("root.d", "s0", false));
+        if (!removeSecondMeasurement) {
+          paths.add(new Path("root.d", "s1", false));
+        }
+        QueryDataSet dataSet = fileReader.query(QueryExpression.create(paths, 
null));
+
+        RowRecord row = dataSet.next();
+        assertEquals(1, row.getTimestamp());
+        assertEquals(10L, row.getFields().get(0).getLongV());
+        if (!removeSecondMeasurement) {
+          assertEquals(100L, row.getFields().get(1).getLongV());
+        }
+
+        row = dataSet.next();
+        assertEquals(2, row.getTimestamp());
+        assertEquals(20L, row.getFields().get(0).getLongV());
+        if (!removeSecondMeasurement) {
+          assertEquals(200L, row.getFields().get(1).getLongV());
+        }
+
+        row = dataSet.next();
+        assertEquals(3, row.getTimestamp());
+        assertEquals(30L, row.getFields().get(0).getLongV());
+        if (!removeSecondMeasurement) {
+          assertTrue(
+              row.getFields().get(1) == null || 
row.getFields().get(1).getDataType() == null);
+        }
+        assertFalse(dataSet.hasNext());
+      }
+    } finally {
+      memChunk.release();
+    }
+  }
+
   @Test
   public void 
testAlignedFastPathKeepsPagesAndValuesAlignedAfterPartialSegmentSort()
       throws IOException, InterruptedException {

Reply via email to