This is an automated email from the ASF dual-hosted git repository.
JackieTien97 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 17e3b4e9460 clone partial columns of aligned tvlist for query (#18409)
17e3b4e9460 is described below
commit 17e3b4e9460c0417f28be525545289eaeea3ed1f
Author: shuwenwei <[email protected]>
AuthorDate: Tue Aug 11 14:27:10 2026 +0800
clone partial columns of aligned tvlist for query (#18409)
---
.../planner/memory/MemoryReservationManager.java | 6 +
.../rate/GroupedRateAccumulatorMemoryTest.java | 5 +
.../rate/RateAccumulatorFactoryTest.java | 3 +
.../RateFunctionIntermediateStateCodecTest.java | 5 +
.../apache/iotdb/db/i18n/DataNodeMiscMessages.java | 19 +
.../apache/iotdb/db/i18n/DataNodeMiscMessages.java | 19 +
.../fragment/FragmentInstanceContext.java | 51 +-
.../execution/fragment/QueryContext.java | 11 +
.../memory/FakedMemoryReservationManager.java | 3 +
.../NotThreadSafeMemoryReservationManager.java | 16 +-
.../memory/ThreadSafeMemoryReservationManager.java | 5 +
.../schemaregion/utils/ResourceByPathUtils.java | 339 +++++++++----
.../memtable/AbstractWritableMemChunk.java | 26 +-
.../memtable/AlignedReadOnlyMemChunk.java | 4 +-
.../dataregion/memtable/ReadOnlyMemChunk.java | 4 +-
.../db/utils/datastructure/AlignedTVList.java | 536 +++++++++++++++++++--
.../db/utils/datastructure/BackAlignedTVList.java | 6 +-
.../db/utils/datastructure/QuickAlignedTVList.java | 6 +-
.../iotdb/db/utils/datastructure/TVList.java | 11 +
.../db/utils/datastructure/TimAlignedTVList.java | 6 +-
.../fragment/FragmentInstanceExecutionTest.java | 212 +++++++-
.../fragment/QueryModificationLoaderTest.java | 5 +
.../LocalExecutionPlannerOperatorsMemoryTest.java | 46 ++
.../utils/ResourceByPathUtilsTest.java | 113 +++++
.../dataregion/memtable/TsFileProcessorTest.java | 58 ++-
.../db/utils/datastructure/AlignedTVListTest.java | 217 ++++++++-
26 files changed, 1525 insertions(+), 207 deletions(-)
diff --git
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/memory/MemoryReservationManager.java
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/memory/MemoryReservationManager.java
index f0420330652..18ad895bb31 100644
---
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/memory/MemoryReservationManager.java
+++
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/memory/MemoryReservationManager.java
@@ -48,6 +48,12 @@ public interface MemoryReservationManager {
*/
void releaseMemoryCumulatively(final long size);
+ /**
+ * Release the given size immediately. This is used to roll back a
reservation when the operation
+ * protected by that reservation fails before ownership is published.
+ */
+ void releaseMemoryImmediately(final long size);
+
/**
* Release all reserved memory immediately. Make sure this method is called
when the lifecycle of
* this manager ends, Or the memory to be released in the batch may not be
released correctly.
diff --git
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/grouped/rate/GroupedRateAccumulatorMemoryTest.java
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/grouped/rate/GroupedRateAccumulatorMemoryTest.java
index 7aa15ebfdea..54c0dc4478a 100644
---
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/grouped/rate/GroupedRateAccumulatorMemoryTest.java
+++
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/grouped/rate/GroupedRateAccumulatorMemoryTest.java
@@ -74,6 +74,11 @@ public class GroupedRateAccumulatorMemoryTest {
cumulativeRelease += size;
}
+ @Override
+ public void releaseMemoryImmediately(long size) {
+ cumulativeRelease += size;
+ }
+
@Override
public void releaseAllReservedMemory() {}
diff --git
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateAccumulatorFactoryTest.java
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateAccumulatorFactoryTest.java
index 364e4439c26..e2556a3cc03 100644
---
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateAccumulatorFactoryTest.java
+++
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateAccumulatorFactoryTest.java
@@ -161,6 +161,9 @@ public class RateAccumulatorFactoryTest {
@Override
public void releaseMemoryCumulatively(long size) {}
+ @Override
+ public void releaseMemoryImmediately(long size) {}
+
@Override
public void releaseAllReservedMemory() {}
diff --git
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateFunctionIntermediateStateCodecTest.java
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateFunctionIntermediateStateCodecTest.java
index ea535e05c5c..aed2a17d96c 100644
---
a/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateFunctionIntermediateStateCodecTest.java
+++
b/iotdb-core/calc-commons/src/test/java/org/apache/iotdb/calc/execution/operator/source/relational/aggregation/rate/RateFunctionIntermediateStateCodecTest.java
@@ -128,6 +128,11 @@ public class RateFunctionIntermediateStateCodecTest {
outstandingReservation -= size;
}
+ @Override
+ public void releaseMemoryImmediately(long size) {
+ outstandingReservation -= size;
+ }
+
@Override
public void releaseAllReservedMemory() {
outstandingReservation = 0;
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index 080fabdc546..e76211672ff 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -1512,4 +1512,23 @@ public final class DataNodeMiscMessages {
public static final String
MISC_EXCEPTION_FAILED_TO_RESOLVE_CANONICAL_PATH_FOR_ACTIVE_LOAD_LISTENING_DIRECTORY_S_ARG_0E6A508E
=
"Failed to resolve canonical path for active load listening directory
%s: %s";
+ public static final String EXCEPTION_COLUMNSTOCLONE_CANNOT_BE_NULL_458FDF37 =
+ "columnsToClone cannot be null";
+ public static final String EXCEPTION_CLONELIST_CANNOT_BE_NULL_47AEEA8F =
+ "cloneList cannot be null";
+ public static final String
EXCEPTION_TARGET_ALIGNEDTVLIST_HAS_INCOMPATIBLE_COLUMN_CONTAINERS_31FAC613 =
+ "Target AlignedTVList has incompatible column containers";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MOVE_08D46037
=
+ "Missing value arrays for aligned column index %d during move";
+ public static final String
EXCEPTION_TARGET_VALUE_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_7889C74F =
+ "Target value column index %d is not ready for move";
+ public static final String
EXCEPTION_TARGET_BITMAP_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_AE3B5F88 =
+ "Target bitmap column index %d is not ready for move";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_CLONE_795EB1C5
=
+ "Missing value arrays for aligned column index %d during clone";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_EXPAND_68E0C8B6
=
+ "Missing value arrays for aligned column index %d during expand";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MARK_NULL_VALUE_2893628E
=
+ "Missing value arrays for aligned column index %d during mark null
value";
+
}
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index 346dc8dafc1..c9a3f49710f 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -1489,4 +1489,23 @@ public final class DataNodeMiscMessages {
public static final String
MISC_EXCEPTION_FAILED_TO_RESOLVE_CANONICAL_PATH_FOR_ACTIVE_LOAD_LISTENING_DIRECTORY_S_ARG_0E6A508E
=
"无法解析 Active Load 监听目录 %s 的 canonical 路径:%s";
+ public static final String EXCEPTION_COLUMNSTOCLONE_CANNOT_BE_NULL_458FDF37 =
+ "columnsToClone 不能为 null";
+ public static final String EXCEPTION_CLONELIST_CANNOT_BE_NULL_47AEEA8F =
+ "cloneList 不能为 null";
+ public static final String
EXCEPTION_TARGET_ALIGNEDTVLIST_HAS_INCOMPATIBLE_COLUMN_CONTAINERS_31FAC613 =
+ "目标 AlignedTVList 的列容器不兼容";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MOVE_08D46037
=
+ "移动过程中缺少对齐列索引 %d 的值数组";
+ public static final String
EXCEPTION_TARGET_VALUE_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_7889C74F =
+ "目标值列索引 %d 尚未准备好进行移动";
+ public static final String
EXCEPTION_TARGET_BITMAP_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_AE3B5F88 =
+ "目标 bitmap 列索引 %d 尚未准备好进行移动";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_CLONE_795EB1C5
=
+ "克隆过程中缺少对齐列索引 %d 的值数组";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_EXPAND_68E0C8B6
=
+ "扩容过程中缺少对齐列索引 %d 的值数组";
+ public static final String
EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MARK_NULL_VALUE_2893628E
=
+ "标记空值过程中缺少对齐列索引 %d 的值数组";
+
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
index e29e2a2141a..b62e6a50990 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
@@ -78,8 +78,10 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
@@ -184,6 +186,9 @@ public class FragmentInstanceContext extends QueryContext {
private long closedUnseqFileNum = 0;
private boolean highestPriority = false;
+ // accessed value columns on each referenced AlignedTVList.
+ private Map<TVList, Set<Integer>> alignedTVListColumnAccessMap = new
ConcurrentHashMap<>();
+
public static FragmentInstanceContext createFragmentInstanceContext(
FragmentInstanceId id,
FragmentInstanceStateMachine stateMachine,
@@ -253,6 +258,49 @@ public class FragmentInstanceContext extends QueryContext {
return queryDataSourceType == QueryDataSourceType.EXTERNAL_TSFILE_SCAN;
}
+ /**
+ * Record columns of the AlignedTVList accessed by the query. This method is
called from
+ * prepareTvListMapForQuery with tvList.lockQueryList() held. Even though
the HashSet inside
+ * alignedTVListColumnAccessMap is not thread-safe, the calling pattern
guarantees thread safety
+ * without requiring additional synchronization.
+ *
+ * @param tvList the TVList being accessed
+ * @param columnIndexList list of column indices being accessed
+ */
+ public void putAccessedColumns(TVList tvList, List<Integer> columnIndexList)
{
+ Set<Integer> accessedColumns =
+ alignedTVListColumnAccessMap.computeIfAbsent(tvList, ignored -> new
HashSet<>());
+ columnIndexList.stream()
+ .filter(Objects::nonNull)
+ .forEach(
+ index -> {
+ if (index >= 0) {
+ accessedColumns.add(index);
+ }
+ });
+ }
+
+ /** Remove column-access metadata for an unpublished TVList when clone
preparation fails. */
+ public void removeAccessedColumns(TVList tvList) {
+ alignedTVListColumnAccessMap.remove(tvList);
+ }
+
+ /**
+ * Get columns of the AlignedTVList accessed by the query. This method is
called from
+ * prepareTvListMapForQuery with tvList.lockQueryList() held, ensuring that
no other thread can
+ * change accessed columns for the same TVList concurrently.
+ *
+ * @param tvList the TVList being accessed
+ * @return set of column indices being accessed, or null if the TVList is
not tracked by this
+ * query. An empty (non-null) set means the TVList is tracked but only
the time column is
+ * accessed (e.g. a time-only scan), which is different from being
untracked.
+ */
+ @Override
+ public Set<Integer> getAccessedAlignedColumns(TVList tvList) {
+ Set<Integer> accessedColumns = alignedTVListColumnAccessMap.get(tvList);
+ return accessedColumns == null ? null :
Collections.unmodifiableSet(accessedColumns);
+ }
+
@TestOnly
public static FragmentInstanceContext createFragmentInstanceContext(
FragmentInstanceId id, FragmentInstanceStateMachine stateMachine) {
@@ -1041,12 +1089,12 @@ public class FragmentInstanceContext extends
QueryContext {
*/
private void releaseTVListOwnedByQuery() {
for (TVList tvList : tvListSet) {
- long tvListRamSize = tvList.calculateRamSize().getRamSize();
tvList.lockQueryList();
Set<QueryContext> queryContextSet = tvList.getQueryContextSet();
try {
queryContextSet.remove(this);
if (tvList.getOwnerQuery() == this) {
+ long tvListRamSize = tvList.calculateRamSize().getRamSize();
if (tvList.getReservedMemoryBytes() != tvListRamSize) {
LOGGER.warn(
DataNodeQueryMessages
@@ -1131,6 +1179,7 @@ public class FragmentInstanceContext extends QueryContext
{
// release TVList/AlignedTVList owned by current query
releaseTVListOwnedByQuery();
+ alignedTVListColumnAccessMap = null;
fileModCache = null;
tables = null;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryContext.java
index 83b567b0650..a9cea56e96e 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryContext.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryContext.java
@@ -282,6 +282,17 @@ public class QueryContext {
tvListSet.addAll(set);
}
+ /**
+ * Get columns of the TVList accessed by this query, or null if the query
does not track
+ * column-level access for the TVList (e.g. a non-FragmentInstanceContext
query, or a TVList not
+ * tracked by a FragmentInstanceContext). An empty (non-null) set means the
TVList is tracked but
+ * only the time column is accessed (e.g. a time-only scan), which is
different from being
+ * untracked.
+ */
+ public Set<Integer> getAccessedAlignedColumns(TVList tvList) {
+ return null;
+ }
+
public void addRowLevelFilteredCount(long count) {
throw new UnsupportedOperationException(
DataNodeQueryMessages
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
index d1c34e365ef..8b45582f138 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
@@ -37,6 +37,9 @@ public class FakedMemoryReservationManager implements
MemoryReservationManager {
@Override
public void releaseMemoryCumulatively(long size) {}
+ @Override
+ public void releaseMemoryImmediately(long size) {}
+
@Override
public void releaseAllReservedMemory() {}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/NotThreadSafeMemoryReservationManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/NotThreadSafeMemoryReservationManager.java
index 71924894c7c..39e987a4b2f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/NotThreadSafeMemoryReservationManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/NotThreadSafeMemoryReservationManager.java
@@ -80,7 +80,14 @@ public class NotThreadSafeMemoryReservationManager
implements MemoryReservationM
public void reserveMemoryCumulatively(final long size) {
bytesToBeReserved += size;
if (bytesToBeReserved >= MEMORY_BATCH_THRESHOLD) {
- reserveMemoryImmediately();
+ try {
+ reserveMemoryImmediately();
+ } catch (RuntimeException | Error failure) {
+ // reserveMemoryImmediately can fail only while asking the planner for
memory, before it
+ // updates this manager's counters. Keep the caller-visible
reservation operation atomic.
+ bytesToBeReserved -= size;
+ throw failure;
+ }
}
}
@@ -129,6 +136,13 @@ public class NotThreadSafeMemoryReservationManager
implements MemoryReservationM
}
}
+ @Override
+ public void releaseMemoryImmediately(final long size) {
+ if (size > 0) {
+ releaseBytesImmediately(size);
+ }
+ }
+
private void releaseBytesImmediately(final long size) {
long poolBytes = deductReleaseAccounting(size);
if (poolBytes > 0) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/ThreadSafeMemoryReservationManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/ThreadSafeMemoryReservationManager.java
index 0a1c6eee418..71676e5b77f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/ThreadSafeMemoryReservationManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/ThreadSafeMemoryReservationManager.java
@@ -51,6 +51,11 @@ public class ThreadSafeMemoryReservationManager extends
NotThreadSafeMemoryReser
super.releaseMemoryCumulatively(size);
}
+ @Override
+ public synchronized void releaseMemoryImmediately(long size) {
+ super.releaseMemoryImmediately(size);
+ }
+
@Override
public synchronized void releaseAllReservedMemory() {
super.releaseAllReservedMemory();
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtils.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtils.java
index 17d5dfd2995..f95fcfda473 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtils.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtils.java
@@ -40,6 +40,7 @@ import
org.apache.iotdb.db.storageengine.dataregion.modification.ModEntry;
import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
import org.apache.iotdb.db.utils.ModificationUtils;
import org.apache.iotdb.db.utils.SchemaUtils;
+import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
import org.apache.iotdb.db.utils.datastructure.TVList;
import org.apache.tsfile.enums.TSDataType;
@@ -73,6 +74,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.iotdb.commons.path.AlignedPath.VECTOR_PLACEHOLDER;
@@ -129,7 +131,8 @@ public abstract class ResourceByPathUtils {
QueryContext context,
IWritableMemChunk memChunk,
boolean isWorkMemTable,
- Filter globalTimeFilter) {
+ Filter globalTimeFilter,
+ List<Integer> columnIndexList) {
// should copy globalTimeFilter because GroupByMonthFilter is stateful
Filter copyTimeFilter = null;
if (globalTimeFilter != null) {
@@ -150,121 +153,249 @@ public abstract class ResourceByPathUtils {
.SCHEMA_LOG_FLUSHING_WORKING_MEMTABLE_ADD_CURRENT_QUERY_CONTEXT_TO_IMMUTABLE_7B7CD373);
tvList.getQueryContextSet().add(context);
tvListQueryMap.put(tvList, tvList.rowCount());
+ // columnIndexList is to track column-level access for AlignedTVList.
+ // For TVList (primitive time series), it remains null and column
tracking is not needed.
+ if (columnIndexList != null && context instanceof
FragmentInstanceContext) {
+ ((FragmentInstanceContext) context).putAccessedColumns(tvList,
columnIndexList);
+ }
} finally {
tvList.unlockQueryList();
}
}
- // mutable tvlist
- TVList list = memChunk.getWorkingTVList();
- TVList cloneList = null;
- TVList.RamInfo listRamInfo = list.calculateRamSize();
- list.lockQueryList();
- try {
- if (copyTimeFilter != null
- && !copyTimeFilter.satisfyStartEndTime(list.getMinTime(),
list.getMaxTime())) {
- return tvListQueryMap;
- }
+ TVList.RamInfo listRamInfo = null;
+
+ // calculateRamSize (synchronized method on TVList) was previously called
before
+ // lockQueryList to avoid deadlock concerns. For partial clone of
AlignedTVList, however
+ // calculateRamSize must now be called inside the lockQueryList section
because it depends on
+ // accessing columns on the AlignedTVList.
+ // This is safe because the lock ordering — queryListLock must always be
acquired before the
+ // TVList intrinsic lock (via synchronized methods like calculateRamSize,
clone). So no AB-BA
+ // deadlock is possible.
+ while (true) {
+ // The working TVList may be replaced by a concurrent query via
clone-and-swap
+ // (memChunk.setWorkingTVList(clone)). A queryListLock held on a
detached candidate does
+ // not protect the current working TVList, so after acquiring the lock,
re-verify it is
+ // still the current working list under the memChunk lock. If it was
replaced while
+ // waiting for candidate's queryListLock, retry with the current one.
+ final TVList candidate = memChunk.getWorkingTVList();
+ candidate.lockQueryList();
+ try {
+ synchronized (memChunk) {
+ if (memChunk.getWorkingTVList() != candidate) {
+ continue;
+ }
+ }
- if (!isWorkMemTable) {
- /*
- * 1. Q1 queries this TVList while it is still in the working memtable
and records a smaller
- * visible row count.
- * 2. Later writes append out-of-order rows to the same TVList, then
FLUSH moves the
- * memtable to the flushing list.
- * 3. Q2 queries the flushing memtable. If Q2 directly reuses the
original mutable TVList,
- * Q2's query-side sort may reorder the indices in place.
- * 4. Q1 continues to read with its old row count and the reordered
indices. The converted
- * value index can exceed Q1's bitmap range and cause out-of-bound
access.
- *
- * Therefore, this flushing branch can reuse the original list only
when it is already
- * sorted or no active query is using it. Otherwise, Q2 should read
from
- * workingListForFlush.
- */
- boolean canUseListDirectly = list.isSorted() ||
list.getQueryContextSet().isEmpty();
- LOGGER.debug(
- DataNodeSchemaMessages
-
.SCHEMA_LOG_FLUSHING_MEMTABLE_ADD_CURRENT_QUERY_CONTEXT_TO_MUTABLE_TVLIST_BEB0D766);
- if (canUseListDirectly) {
- list.getQueryContextSet().add(context);
- tvListQueryMap.put(list, list.rowCount());
- } else {
- TVList workingListForFlushSort =
memChunk.initWorkingListForFlushIfNecessary(list, true);
+ if (copyTimeFilter != null
+ && !copyTimeFilter.satisfyStartEndTime(
+ candidate.getMinTime(), candidate.getMaxTime())) {
+ return tvListQueryMap;
+ }
+
+ if (!isWorkMemTable) {
/*
- * The query will read from workingListForFlushSort, but
cloneForFlushSort() only clones
- * times and indices. The value arrays and bitmaps are still shared
with the original
- * list.
- *
- * Therefore, this query must also hold the original list until it
finishes. Adding
- * context to list.getQueryContextSet() lets flush/query cleanup see
that the original
- * list is still in use. Adding list to context.tvListSet makes
- * releaseTVListOwnedByQuery() remove this context from the original
list later.
+ * 1. Q1 queries this TVList while it is still in the working
memtable and records a smaller
+ * visible row count.
+ * 2. Later writes append out-of-order rows to the same TVList, then
FLUSH moves the
+ * memtable to the flushing list.
+ * 3. Q2 queries the flushing memtable. If Q2 directly reuses the
original mutable TVList,
+ * Q2's query-side sort may reorder the indices in place.
+ * 4. Q1 continues to read with its old row count and the reordered
indices. The converted
+ * value index can exceed Q1's bitmap range and cause
out-of-bound access.
*
- * Do not put the original list into tvListQueryMap here. The actual
read path must use
- * workingListForFlushSort to avoid sorting the original list in
place.
+ * Therefore, this flushing branch can reuse the original list only
when it is already
+ * sorted or no active query is using it. Otherwise, Q2 should read
from
+ * workingListForFlush.
*/
- list.getQueryContextSet().add(context);
- context.addTVListToSet(Collections.singleton(list));
- workingListForFlushSort.getQueryContextSet().add(context);
- tvListQueryMap.put(workingListForFlushSort,
workingListForFlushSort.rowCount());
- }
- } else {
- if (list.isSorted() || list.getQueryContextSet().isEmpty()) {
+ boolean canUseListDirectly =
+ candidate.isSorted() || candidate.getQueryContextSet().isEmpty();
LOGGER.debug(
DataNodeSchemaMessages
-
.SCHEMA_LOG_WORKING_MEMTABLE_ADD_CURRENT_QUERY_CONTEXT_TO_MUTABLE_TVLIST_8C937414);
- list.getQueryContextSet().add(context);
- tvListQueryMap.put(list, list.rowCount());
- } else {
- /*
- * +----------------------+
- * | MemTable |
- * | |
- * | +------------+ | +-----------------+
- * | | TVList |<---+--+ +---+ Previous Query |
- * | +-----^------+ | | | +-----------------+
- * | | | | |
- * +----------+-----------+ | | +----------------+
- * | Clone +---+---+ Current Query |
- * +-----+------+ | +----------------+
- * | TVList | <---------+
- * +------------+
- */
+
.SCHEMA_LOG_FLUSHING_MEMTABLE_ADD_CURRENT_QUERY_CONTEXT_TO_MUTABLE_TVLIST_BEB0D766);
+ if (canUseListDirectly) {
+ candidate.getQueryContextSet().add(context);
+ tvListQueryMap.put(candidate, candidate.rowCount());
+ } else {
+ TVList workingListForFlushSort =
+ memChunk.initWorkingListForFlushIfNecessary(candidate, true);
+ /*
+ * The query will read from workingListForFlushSort, but
cloneForFlushSort() only clones
+ * times and indices. The value arrays and bitmaps are still
shared with the original
+ * list.
+ *
+ * Therefore, this query must also hold the original list until it
finishes. Adding
+ * context to list.getQueryContextSet() lets flush/query cleanup
see that the original
+ * list is still in use. Adding list to context.tvListSet makes
+ * releaseTVListOwnedByQuery() remove this context from the
original list later.
+ *
+ * Do not put the original list into tvListQueryMap here. The
actual read path must use
+ * workingListForFlushSort to avoid sorting the original list in
place.
+ */
+ candidate.getQueryContextSet().add(context);
+ context.addTVListToSet(Collections.singleton(candidate));
+ // Query preparation is serialized by candidate's query-list lock,
but cleanup removes
+ // the context under workingListForFlushSort's own lock. Use the
same lock for this add
+ // to avoid concurrently mutating its HashSet. The lock order here
is candidate first,
+ // then workingListForFlushSort; cleanup never holds both locks at
the same time.
+ workingListForFlushSort.lockQueryList();
+ try {
+ workingListForFlushSort.getQueryContextSet().add(context);
+ } finally {
+ workingListForFlushSort.unlockQueryList();
+ }
+ tvListQueryMap.put(workingListForFlushSort,
workingListForFlushSort.rowCount());
+ }
+
+ // columnIndexList is to track column-level access for AlignedTVList.
+ // For TVList (primitive time series), it remains null and column
tracking is not needed.
+ if (columnIndexList != null && context instanceof
FragmentInstanceContext) {
+ ((FragmentInstanceContext) context).putAccessedColumns(candidate,
columnIndexList);
+ }
+ return tvListQueryMap;
+ }
+
+ if (candidate.isSorted() || candidate.getQueryContextSet().isEmpty()) {
LOGGER.debug(
DataNodeSchemaMessages
-
.SCHEMA_LOG_WORKING_MEMTABLE_CLONE_MUTABLE_TVLIST_AND_REPLACE_OLD_TVLIST_FD1EAE22);
- QueryContext firstQuery =
list.getQueryContextSet().iterator().next();
- // reserve query memory
- if (firstQuery instanceof FragmentInstanceContext) {
- MemoryReservationManager memoryReservationManager =
- ((FragmentInstanceContext)
firstQuery).getMemoryReservationContext();
-
memoryReservationManager.reserveMemoryCumulatively(listRamInfo.getRamSize());
- list.setReservedMemoryBytes(listRamInfo.getRamSize());
+
.SCHEMA_LOG_WORKING_MEMTABLE_ADD_CURRENT_QUERY_CONTEXT_TO_MUTABLE_TVLIST_8C937414);
+ candidate.getQueryContextSet().add(context);
+ tvListQueryMap.put(candidate, candidate.rowCount());
+
+ // columnIndexList is to track column-level access for AlignedTVList.
+ // For TVList (primitive time series), it remains null and column
tracking is not needed.
+ if (columnIndexList != null && context instanceof
FragmentInstanceContext) {
+ ((FragmentInstanceContext) context).putAccessedColumns(candidate,
columnIndexList);
+ }
+ return tvListQueryMap;
+ }
+
+ /*
+ * +----------------------+
+ * | MemTable |
+ * | |
+ * | +------------+ | +-----------------+
+ * | | TVList |<---+--+ +---+ Previous Query |
+ * | +-----^------+ | | | +-----------------+
+ * | | | | |
+ * +----------+-----------+ | | +----------------+
+ * | Clone +---+---+ Current Query |
+ * +-----+------+ | +----------------+
+ * | TVList | <---------+
+ * +------------+
+ */
+ LOGGER.debug(
+ DataNodeSchemaMessages
+
.SCHEMA_LOG_WORKING_MEMTABLE_CLONE_MUTABLE_TVLIST_AND_REPLACE_OLD_TVLIST_FD1EAE22);
+
+ synchronized (memChunk) {
+ // Re-check defensively before cloning and publishing the
replacement. The clone and the
+ // working-list swap must be done in the same memChunk critical
section, so a concurrent
+ // query can never observe a working TVList whose columns have
already been moved away.
+ if (memChunk.getWorkingTVList() != candidate) {
+ continue;
}
- list.setOwnerQuery(firstQuery);
- // clone TVList
- cloneList = list.clone();
- cloneList.getQueryContextSet().add(context);
- tvListQueryMap.put(cloneList, cloneList.rowCount());
+ // calculateRamSize (synchronized method on TVList) was previously
called before
+ // lockQueryList to avoid deadlock concerns. For partial clone of
AlignedTVList, however
+ // calculateRamSize must now be called inside the lockQueryList
section because it depends
+ // on accessing columns on the AlignedTVList.
+ // This is safe because the lock ordering - queryListLock must
always be acquired before
+ // the TVList intrinsic lock (via synchronized methods like
calculateRamSize, clone). So
+ // no AB-BA deadlock is possible.
+ Set<Integer> columnsToClone = candidate.getAccessedColumnsForQuery();
+ listRamInfo =
+ (columnsToClone == null)
+ ? candidate.calculateRamSize()
+ : ((AlignedTVList)
candidate).calculateRamSize(columnsToClone);
+
+ QueryContext firstQuery =
candidate.getQueryContextSet().iterator().next();
+ TVList cloneList = null;
+ AlignedTVList.PartialClonePlan partialClonePlan = null;
+ FragmentInstanceContext cloneContext =
+ columnIndexList != null && context instanceof
FragmentInstanceContext
+ ? (FragmentInstanceContext) context
+ : null;
+ MemoryReservationManager memoryReservationManager =
+ firstQuery instanceof FragmentInstanceContext
+ ? ((FragmentInstanceContext)
firstQuery).getMemoryReservationContext()
+ : null;
+ boolean reservationNeedsRollback = false;
+ boolean replacementPublished = false;
+ try {
+ // Reserve before allocating the clone, so this transient memory
increase is still
+ // protected by query-memory admission control. Ownership is not
published yet, and a
+ // later preparation failure rolls this exact reservation back
immediately.
+ if (memoryReservationManager != null) {
+
memoryReservationManager.reserveMemoryCumulatively(listRamInfo.getRamSize());
+ reservationNeedsRollback = true;
+ }
+
+ // Clone and validate without changing the source list.
PartialClonePlan.commit is the
+ // only destructive step and is allocation-free.
+ if (columnsToClone == null) {
+ cloneList = candidate.clone();
+ } else {
+ partialClonePlan = ((AlignedTVList)
candidate).preparePartialClone(columnsToClone);
+ cloneList = partialClonePlan.getCloneList();
+ }
+
+ cloneList.getQueryContextSet().add(context);
+ tvListQueryMap.put(cloneList, cloneList.rowCount());
+ if (cloneContext != null) {
+ cloneContext.putAccessedColumns(cloneList, columnIndexList);
+ }
+
+ if (partialClonePlan != null) {
+ partialClonePlan.commit();
+ }
+ memChunk.setWorkingTVList(cloneList);
+ replacementPublished = true;
+
+ // Publish query ownership only after the replacement is fully
committed. The
+ // candidate query-list lock prevents its owner from being
released concurrently.
+ if (memoryReservationManager != null) {
+ candidate.setReservedMemoryBytes(listRamInfo.getRamSize());
+ }
+ candidate.setOwnerQuery(firstQuery);
+ reservationNeedsRollback = false;
+ return tvListQueryMap;
+ } catch (RuntimeException | Error failure) {
+ if (reservationNeedsRollback) {
+ try {
+
memoryReservationManager.releaseMemoryImmediately(listRamInfo.getRamSize());
+ } catch (RuntimeException | Error rollbackFailure) {
+ failure.addSuppressed(rollbackFailure);
+ }
+ }
+
+ // Before commit, remove the only external reference installed for
the unpublished
+ // clone. Its arrays can then be reclaimed while candidate remains
the working list.
+ if (!replacementPublished && cloneList != null) {
+ cloneList.getQueryContextSet().remove(context);
+ tvListQueryMap.remove(cloneList);
+ if (cloneContext != null) {
+ cloneContext.removeAccessedColumns(cloneList);
+ }
+ }
+ throw failure;
+ }
}
+ } catch (MemoryNotEnoughException ex) {
+ if (listRamInfo != null) {
+ LOGGER.warn(
+ DataNodeSchemaMessages.FAILED_TO_RESERVE_MEMORY_TVLIST,
+ listRamInfo.getRamSize(),
+ listRamInfo.getTimestampsSize(),
+ listRamInfo.getArrayMemCost(),
+ listRamInfo.getRowCount(),
+ listRamInfo.getDataTypes());
+ }
+ throw ex;
+ } finally {
+ candidate.unlockQueryList();
}
- } catch (MemoryNotEnoughException ex) {
- LOGGER.warn(
- DataNodeSchemaMessages.FAILED_TO_RESERVE_MEMORY_TVLIST,
- listRamInfo.getRamSize(),
- listRamInfo.getTimestampsSize(),
- listRamInfo.getArrayMemCost(),
- listRamInfo.getRowCount(),
- listRamInfo.getDataTypes());
- throw ex;
- } finally {
- list.unlockQueryList();
}
- if (cloneList != null) {
- memChunk.setWorkingTVList(cloneList);
- }
- return tvListQueryMap;
}
}
@@ -451,11 +582,6 @@ class AlignedResourceByPathUtils extends
ResourceByPathUtils {
}
}
- // prepare AlignedTVList for query. It should clone TVList if necessary.
- Map<TVList, Integer> alignedTvListQueryMap =
- prepareTvListMapForQuery(
- context, alignedMemChunk, modsToMemtable == null,
globalTimeFilter);
-
// column index list for the query
// Columns with inconsistent types will be ignored and set -1
List<Integer> columnIndexList =
@@ -463,6 +589,11 @@ class AlignedResourceByPathUtils extends
ResourceByPathUtils {
List<TimeRange> timeColumnDeletion = null;
List<List<TimeRange>> valueColumnsDeletionList = null;
+ // prepare AlignedTVList for query. It should clone TVList if necessary.
+ Map<TVList, Integer> alignedTvListQueryMap =
+ prepareTvListMapForQuery(
+ context, alignedMemChunk, modsToMemtable == null,
globalTimeFilter, columnIndexList);
+
if (modsToMemtable != null) {
timeColumnDeletion =
ModificationUtils.constructDeletionList(
@@ -696,7 +827,7 @@ class MeasurementResourceByPathUtils extends
ResourceByPathUtils {
}
// prepare TVList for query. It should clone TVList if necessary.
Map<TVList, Integer> tvListQueryMap =
- prepareTvListMapForQuery(context, memChunk, modsToMemtable == null,
globalTimeFilter);
+ prepareTvListMapForQuery(context, memChunk, modsToMemtable == null,
globalTimeFilter, null);
List<TimeRange> deletionList = null;
if (modsToMemtable != null) {
deletionList =
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AbstractWritableMemChunk.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AbstractWritableMemChunk.java
index 9eb30ec5911..4f2bb0abd11 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AbstractWritableMemChunk.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AbstractWritableMemChunk.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.db.i18n.StorageEngineMessages;
import
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext;
import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext;
import
org.apache.iotdb.db.storageengine.dataregion.wal.buffer.IWALByteBufferView;
+import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
import org.apache.iotdb.db.utils.datastructure.BatchEncodeInfo;
import org.apache.iotdb.db.utils.datastructure.TVList;
@@ -43,6 +44,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Iterator;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.BlockingQueue;
public abstract class AbstractWritableMemChunk implements IWritableMemChunk {
@@ -109,19 +111,41 @@ public abstract class AbstractWritableMemChunk implements
IWritableMemChunk {
}
}
+ /**
+ * Try to release the TVList. If there are active queries, transfer memory
ownership to the first
+ * query. For AlignedTVList, this will release non-query columns before
transferring to reduce
+ * memory footprint.
+ */
private void tryReleaseTvList(TVList tvList) {
- long tvListRamSize = tvList.calculateRamSize().getRamSize();
tvList.lockQueryList();
try {
if (tvList.getQueryContextSet().isEmpty()) {
tvList.clear();
} else {
QueryContext firstQuery =
tvList.getQueryContextSet().iterator().next();
+
+ // For AlignedTVList with active queries, release non-query columns
before
+ // transferring memory ownership to reduce memory footprint.
+ if (tvList instanceof AlignedTVList) {
+ AlignedTVList alignedTVList = (AlignedTVList) tvList;
+
+ // Get the union of all columns accessed by queries. An empty
(non-null) set means all
+ // queries are tracked but only access the time column, so all value
columns are
+ // released; null means some query is untracked and
releaseNonQueryColumns keeps
+ // everything.
+ Set<Integer> accessedColumns =
alignedTVList.getAccessedColumnsForQuery();
+ if (accessedColumns != null) {
+ // Release non-query columns to reduce memory before ownership
transfer
+ alignedTVList.releaseNonQueryColumns(accessedColumns);
+ }
+ }
+
// transfer memory from write process to read process. Here it
reserves read memory and
// releaseFlushedMemTable will release write memory.
if (firstQuery instanceof FragmentInstanceContext) {
MemoryReservationManager memoryReservationManager =
((FragmentInstanceContext)
firstQuery).getMemoryReservationContext();
+ long tvListRamSize = tvList.calculateRamSize().getRamSize();
memoryReservationManager.reserveMemoryCumulatively(tvListRamSize);
tvList.setReservedMemoryBytes(tvListRamSize);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedReadOnlyMemChunk.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedReadOnlyMemChunk.java
index 0ba04ad53e6..55e1899ceeb 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedReadOnlyMemChunk.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedReadOnlyMemChunk.java
@@ -128,12 +128,12 @@ public class AlignedReadOnlyMemChunk extends
ReadOnlyMemChunk {
// We must update queryRowCount here, otherwise, it may be used later
to build
// BitMaps, causing bitmap array size mismatch and possible out of
bound.
entry.setValue(alignedTvList.sort());
- long alignedTvListRamSize =
alignedTvList.calculateRamSize().getRamSize();
alignedTvList.lockQueryList();
try {
FragmentInstanceContext ownerQuery =
(FragmentInstanceContext) alignedTvList.getOwnerQuery();
if (ownerQuery != null) {
+ long alignedTvListRamSize =
alignedTvList.calculateRamSize().getRamSize();
long deltaBytes = alignedTvListRamSize -
alignedTvList.getReservedMemoryBytes();
if (deltaBytes > 0) {
ownerQuery.getMemoryReservationContext().reserveMemoryCumulatively(deltaBytes);
@@ -387,12 +387,12 @@ public class AlignedReadOnlyMemChunk extends
ReadOnlyMemChunk {
int queryLength = entry.getValue();
if (!alignedTvList.isSorted() && queryLength >
alignedTvList.seqRowCount()) {
entry.setValue(alignedTvList.sort());
- long alignedTvListRamSize =
alignedTvList.calculateRamSize().getRamSize();
alignedTvList.lockQueryList();
try {
FragmentInstanceContext ownerQuery =
(FragmentInstanceContext) alignedTvList.getOwnerQuery();
if (ownerQuery != null) {
+ long alignedTvListRamSize =
alignedTvList.calculateRamSize().getRamSize();
long deltaBytes = alignedTvListRamSize -
alignedTvList.getReservedMemoryBytes();
if (deltaBytes > 0) {
ownerQuery.getMemoryReservationContext().reserveMemoryCumulatively(deltaBytes);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/ReadOnlyMemChunk.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/ReadOnlyMemChunk.java
index 39629bbbcaa..bfefb9817bb 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/ReadOnlyMemChunk.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/ReadOnlyMemChunk.java
@@ -139,11 +139,11 @@ public class ReadOnlyMemChunk {
int queryRowCount = entry.getValue();
if (!tvList.isSorted() && queryRowCount > tvList.seqRowCount()) {
entry.setValue(tvList.sort());
- long tvListRamSize = tvList.calculateRamSize().getRamSize();
tvList.lockQueryList();
try {
FragmentInstanceContext ownerQuery = (FragmentInstanceContext)
tvList.getOwnerQuery();
if (ownerQuery != null) {
+ long tvListRamSize = tvList.calculateRamSize().getRamSize();
long deltaBytes = tvListRamSize - tvList.getReservedMemoryBytes();
if (deltaBytes > 0) {
ownerQuery.getMemoryReservationContext().reserveMemoryCumulatively(deltaBytes);
@@ -298,11 +298,11 @@ public class ReadOnlyMemChunk {
int queryLength = entry.getValue();
if (!tvList.isSorted() && queryLength > tvList.seqRowCount()) {
entry.setValue(tvList.sort());
- long tvListRamSize = tvList.calculateRamSize().getRamSize();
tvList.lockQueryList();
try {
FragmentInstanceContext ownerQuery = (FragmentInstanceContext)
tvList.getOwnerQuery();
if (ownerQuery != null) {
+ long tvListRamSize = tvList.calculateRamSize().getRamSize();
long deltaBytes = tvListRamSize - tvList.getReservedMemoryBytes();
if (deltaBytes > 0) {
ownerQuery.getMemoryReservationContext().reserveMemoryCumulatively(deltaBytes);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
index 035b9a89bb9..608c363552c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/AlignedTVList.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.db.utils.datastructure;
import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.commons.utils.TestOnly;
import org.apache.iotdb.db.i18n.DataNodeMiscMessages;
import org.apache.iotdb.db.i18n.StorageEngineMessages;
import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext;
@@ -46,6 +47,7 @@ import org.apache.tsfile.read.filter.basic.Filter;
import org.apache.tsfile.utils.Binary;
import org.apache.tsfile.utils.BitMap;
import org.apache.tsfile.utils.Pair;
+import org.apache.tsfile.utils.RamUsageEstimator;
import org.apache.tsfile.utils.ReadWriteForEncodingUtils;
import org.apache.tsfile.utils.ReadWriteIOUtils;
import org.apache.tsfile.utils.TsPrimitiveType;
@@ -58,8 +60,10 @@ import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -87,6 +91,49 @@ public abstract class AlignedTVList extends TVList {
private long materializedBitmapMemoryCost;
private long arrayMemCostWithoutPrimitiveArraysAndIndex;
+ /**
+ * A fully prepared partial clone. All allocations and validations are
completed before this plan
+ * is returned, so {@link #commit()} only moves already captured references
and updates primitive
+ * accounting fields.
+ */
+ public static final class PartialClonePlan {
+ private final AlignedTVList sourceList;
+ private final AlignedTVList cloneList;
+ // The columns retained by the source. commit() derives the moved columns
from this set, so
+ // no O(N) move-plan arrays need to be allocated during preparation.
+ private final Set<Integer> retainedColumns;
+ private final long sourceBitmapMemoryCost;
+ private final long cloneBitmapMemoryCost;
+
+ private boolean committed;
+
+ private PartialClonePlan(
+ AlignedTVList sourceList,
+ AlignedTVList cloneList,
+ Set<Integer> retainedColumns,
+ long sourceBitmapMemoryCost,
+ long cloneBitmapMemoryCost) {
+ this.sourceList = sourceList;
+ this.cloneList = cloneList;
+ this.retainedColumns = retainedColumns;
+ this.sourceBitmapMemoryCost = sourceBitmapMemoryCost;
+ this.cloneBitmapMemoryCost = cloneBitmapMemoryCost;
+ }
+
+ public AlignedTVList getCloneList() {
+ return cloneList;
+ }
+
+ /** Commit the prepared ownership transfer. This method is idempotent and
allocation-free. */
+ public synchronized void commit() {
+ if (committed) {
+ return;
+ }
+ sourceList.commitPartialClone(this);
+ committed = true;
+ }
+ }
+
// Data type list -> list of TVList, add 1 when expanded -> primitive array
of basic type.
// A null primitive array means all existing rows in that block are null for
the column.
// Index relation: columnIndex(dataTypeIndex) -> arrayIndex -> elementIndex
@@ -107,26 +154,37 @@ public abstract class AlignedTVList extends TVList {
private final AlignedTVList outer = this;
AlignedTVList(List<TSDataType> types) {
+ this(types, true);
+ }
+
+ AlignedTVList(List<TSDataType> types, boolean initializeValueColumns) {
super();
dataTypes = types;
memoryBinaryChunkSize = new long[dataTypes.size()];
materializedValueArrayCounts = new int[dataTypes.size()];
- refreshArrayMemCostWithoutPrimitiveArrays();
values = new ArrayList<>(types.size());
for (int i = 0; i < types.size(); i++) {
- values.add(new ArrayList<>(getDefaultArrayNum()));
+ values.add(initializeValueColumns ? new
ArrayList<>(getDefaultArrayNum()) : null);
}
+ // arrayMemCostWithoutPrimitiveArrays depends on per-column value arrays,
so values must be
+ // initialized before computing it
+ refreshArrayMemCostWithoutPrimitiveArrays();
}
public static AlignedTVList newAlignedList(List<TSDataType> dataTypes) {
+ return newAlignedList(dataTypes, true);
+ }
+
+ public static AlignedTVList newAlignedList(
+ List<TSDataType> dataTypes, boolean initializeValueColumns) {
switch (TVLIST_SORT_ALGORITHM) {
case QUICK:
- return new QuickAlignedTVList(dataTypes);
+ return new QuickAlignedTVList(dataTypes, initializeValueColumns);
case BACKWARD:
- return new BackAlignedTVList(dataTypes);
+ return new BackAlignedTVList(dataTypes, initializeValueColumns);
default:
- return new TimAlignedTVList(dataTypes);
+ return new TimAlignedTVList(dataTypes, initializeValueColumns);
}
}
@@ -140,6 +198,7 @@ public abstract class AlignedTVList extends TVList {
}
@Override
+ @TestOnly
public TVList getTvListByColumnIndex(
List<Integer> columnIndexList, List<TSDataType> dataTypeList, boolean
ignoreAllNullRows) {
List<List<Object>> values = new ArrayList<>();
@@ -178,17 +237,16 @@ public abstract class AlignedTVList extends TVList {
alignedTvList.allValueColDeletedMap = ignoreAllNullRows ?
getAllValueColDeletedMap() : null;
alignedTvList.timeColDeletedMap = this.timeColDeletedMap;
alignedTvList.timeDeletedCnt = this.timeDeletedCnt;
- alignedTvList.materializedBitmapMemoryCost =
calculateBitmapRamCost(bitMaps);
+ alignedTvList.materializedBitmapMemoryCost =
calculateBitmapRamCost(bitMaps, null);
for (int i = 0; i < columnIndexList.size(); i++) {
int columnIndex = columnIndexList.get(i);
if (columnIndex != -1 && values.get(i) != null) {
int materializedArrayCount = materializedValueArrayCounts[columnIndex];
alignedTvList.materializedValueArrayCounts[i] = materializedArrayCount;
alignedTvList.materializedValueArrayMemCost +=
- (long) materializedArrayCount *
valueListArrayMemCost(dataTypeList.get(i));
+ (long) materializedArrayCount *
primitiveArrayMemCost(dataTypeList.get(i));
}
}
-
return alignedTvList;
}
@@ -212,39 +270,190 @@ public abstract class AlignedTVList extends TVList {
public synchronized AlignedTVList clone() {
AlignedTVList cloneList = AlignedTVList.newAlignedList(new
ArrayList<>(dataTypes));
cloneAs(cloneList);
- cloneList.timeDeletedCnt = this.timeDeletedCnt;
- System.arraycopy(
- memoryBinaryChunkSize, 0, cloneList.memoryBinaryChunkSize, 0,
dataTypes.size());
+ cloneColumnDataTo(cloneList, null);
+ cloneList.materializedValueArrayCounts =
+ Arrays.copyOf(materializedValueArrayCounts,
materializedValueArrayCounts.length);
+ cloneList.materializedValueArrayMemCost = materializedValueArrayMemCost;
+ return cloneList;
+ }
+
+ /**
+ * Prepare a partial clone without changing this TVList. The returned plan
must be committed only
+ * after the query-memory reservation succeeds.
+ */
+ public synchronized PartialClonePlan preparePartialClone(Set<Integer>
columnsToClone) {
+ Set<Integer> retainedColumns =
+ new HashSet<>(
+ Objects.requireNonNull(
+ columnsToClone,
+
DataNodeMiscMessages.EXCEPTION_COLUMNSTOCLONE_CANNOT_BE_NULL_458FDF37));
+ AlignedTVList cloneList = AlignedTVList.newAlignedList(new
ArrayList<>(dataTypes), false);
+ // Pre-create the inner value lists for the retained columns; the other
slots stay null until
+ // the ownership transfer moves the source columns into place.
for (int i = 0; i < values.size(); i++) {
- // Clone value
- List<Object> columnValues = values.get(i);
- for (Object valueArray : columnValues) {
- cloneList.values.get(i).add(cloneValue(dataTypes.get(i), valueArray));
+ if (retainedColumns.contains(i)) {
+ cloneList.values.set(i, new ArrayList<>(values.get(i).size()));
}
- // Clone bitmap in columnIndex
+ }
+ cloneAs(cloneList);
+ cloneColumnDataTo(cloneList, retainedColumns);
+ return prepareMovePlan(cloneList, retainedColumns);
+ }
+
+ private PartialClonePlan prepareMovePlan(AlignedTVList cloneList,
Set<Integer> retainedColumns) {
+ Objects.requireNonNull(
+ cloneList,
DataNodeMiscMessages.EXCEPTION_CLONELIST_CANNOT_BE_NULL_47AEEA8F);
+ int columnCount = values.size();
+ if (cloneList.values.size() != columnCount
+ || cloneList.memoryBinaryChunkSize.length !=
memoryBinaryChunkSize.length) {
+ throw new IllegalStateException(
+ DataNodeMiscMessages
+
.EXCEPTION_TARGET_ALIGNEDTVLIST_HAS_INCOMPATIBLE_COLUMN_CONTAINERS_31FAC613);
+ }
+
+ // Validate the move without allocating any O(N) move-plan arrays;
commit() re-derives the
+ // moved columns from the retained set, which only needs this validation
to be complete.
+ for (int i = 0; i < columnCount; i++) {
+ if (retainedColumns.contains(i)) {
+ continue;
+ }
+
+ if (values.get(i) == null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MOVE_08D46037,
+ i));
+ }
+ if (cloneList.values.get(i) != null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_TARGET_VALUE_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_7889C74F,
+ i));
+ }
+
if (bitMaps != null && bitMaps.get(i) != null) {
- List<BitMap> columnBitMaps = bitMaps.get(i);
- if (cloneList.bitMaps == null) {
- cloneList.bitMaps = new ArrayList<>(dataTypes.size());
- for (int j = 0; j < dataTypes.size(); j++) {
- cloneList.bitMaps.add(null);
- }
+ if (cloneList.bitMaps == null
+ || cloneList.bitMaps.size() != bitMaps.size()
+ || cloneList.bitMaps.get(i) != null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_TARGET_BITMAP_COLUMN_INDEX_ARG_IS_NOT_READY_FOR_MOVE_AE3B5F88,
+ i));
}
- if (cloneList.bitMaps.get(i) == null) {
- List<BitMap> cloneColumnBitMaps = new
ArrayList<>(columnBitMaps.size());
- for (BitMap bitMap : columnBitMaps) {
- cloneColumnBitMaps.add(bitMap == null ? null : bitMap.clone());
- }
- cloneList.bitMaps.set(i, cloneColumnBitMaps);
+ }
+ }
+
+ return new PartialClonePlan(
+ this,
+ cloneList,
+ retainedColumns,
+ calculateBitmapRamCost(bitMaps, retainedColumns),
+ calculateBitmapRamCost(bitMaps, null));
+ }
+
+ private synchronized void commitPartialClone(PartialClonePlan plan) {
+ // The clone keeps the deep-copied retained columns, so copy their
accounting too.
+ for (int i = 0; i < dataTypes.size(); i++) {
+ int materializedCount = materializedValueArrayCounts[i];
+ plan.cloneList.materializedValueArrayCounts[i] = materializedCount;
+ if (materializedCount > 0) {
+ plan.cloneList.materializedValueArrayMemCost +=
+ (long) materializedCount * primitiveArrayMemCost(dataTypes.get(i));
+ }
+ }
+ Set<Integer> retainedColumns = plan.retainedColumns;
+ for (int i = 0; i < dataTypes.size(); i++) {
+ if (retainedColumns.contains(i)) {
+ continue;
+ }
+
+ // Move value arrays and bitmaps from the source to the clone. The clone
was created with
+ // empty column containers, so the moved references are set into place.
+ List<Object> columnValues = values.get(i);
+ if (columnValues == null) {
+ // Defensive: prepareMovePlan already validated that every moved
column is materialized.
+ continue;
+ }
+ plan.cloneList.values.set(i, columnValues);
+ values.set(i, null);
+ List<BitMap> columnBitMaps = bitMaps == null ? null : bitMaps.get(i);
+ if (columnBitMaps != null) {
+ plan.cloneList.bitMaps.set(i, columnBitMaps);
+ bitMaps.set(i, null);
+ }
+ memoryBinaryChunkSize[i] = 0;
+
+ // The source no longer owns the moved column's materialized arrays.
+ int materializedCount = materializedValueArrayCounts[i];
+ materializedValueArrayCounts[i] = 0;
+ if (materializedCount > 0) {
+ materializedValueArrayMemCost -=
+ (long) materializedCount * primitiveArrayMemCost(dataTypes.get(i));
+ }
+ }
+
+ materializedBitmapMemoryCost = plan.sourceBitmapMemoryCost;
+ plan.cloneList.materializedBitmapMemoryCost = plan.cloneBitmapMemoryCost;
+ // Refresh the cached per-block cost after the retained columns changed on
both lists.
+ refreshArrayMemCostWithoutPrimitiveArrays();
+ plan.cloneList.refreshArrayMemCostWithoutPrimitiveArrays();
+ }
+
+ /**
+ * Release memory for non-query columns in this TVList. This is used during
memory ownership
+ * transfer from write process to read process to reduce memory footprint.
Only columns that are
+ * accessed by active queries are retained; all other columns are released.
+ *
+ * @param columnsToKeep set of column indices that are accessed by queries
and should be kept. A
+ * null set means no access information is available and nothing is
released. An empty set
+ * means all queries are tracked but only access the time column, so all
value columns are
+ * released.
+ */
+ public synchronized void releaseNonQueryColumns(Set<Integer> columnsToKeep) {
+ if (columnsToKeep == null) {
+ return;
+ }
+
+ for (int i = 0; i < values.size(); i++) {
+ // Skip columns that should be kept or are already null
+ if (columnsToKeep.contains(i)) {
+ continue;
+ }
+
+ List<Object> columnValues = values.get(i);
+ if (columnValues == null) {
+ continue;
+ }
+
+ // Release memory for non-query columns
+ for (Object dataArray : columnValues) {
+ if (dataArray != null) {
+ PrimitiveArrayManager.release(dataArray);
}
}
+ values.set(i, null);
+ memoryBinaryChunkSize[i] = 0;
+
+ // Release bitmap memory for non-query columns
+ if (bitMaps != null && bitMaps.get(i) != null) {
+ bitMaps.set(i, null);
+ }
+
+ // Remove the released column from the materialized-array accounting
+ int materializedCount = materializedValueArrayCounts[i];
+ materializedValueArrayCounts[i] = 0;
+ if (materializedCount > 0) {
+ materializedValueArrayMemCost -=
+ (long) materializedCount * primitiveArrayMemCost(dataTypes.get(i));
+ }
}
- cloneList.timeColDeletedMap = timeColDeletedMap == null ? null :
timeColDeletedMap.clone();
- cloneList.materializedValueArrayCounts =
- Arrays.copyOf(materializedValueArrayCounts,
materializedValueArrayCounts.length);
- cloneList.materializedValueArrayMemCost = materializedValueArrayMemCost;
- cloneList.materializedBitmapMemoryCost = materializedBitmapMemoryCost;
- return cloneList;
+
+ materializedBitmapMemoryCost = calculateBitmapRamCost(bitMaps,
columnsToKeep);
+ // Refresh the cached per-block cost after releasing columns
+ refreshArrayMemCostWithoutPrimitiveArrays();
}
@SuppressWarnings("squid:S3776") // Suppress high Cognitive Complexity
warning
@@ -618,6 +827,27 @@ public abstract class AlignedTVList extends TVList {
return dataTypes;
}
+ /**
+ * Get the union of all columns accessed by queries on this AlignedTVList.
This method should be
+ * called with queryListLock held for thread safety.
+ *
+ * @return set of accessed column indices, or null if any query on this
TVList is untracked or
+ * does not record accessed columns. An empty (non-null) set means all
queries are tracked but
+ * only access the time column (e.g. a time-only scan), so all value
columns can be released.
+ */
+ @Override
+ public Set<Integer> getAccessedColumnsForQuery() {
+ Set<Integer> accessedColumns = new HashSet<>();
+ for (QueryContext queryContext : getQueryContextSet()) {
+ Set<Integer> columns = queryContext.getAccessedAlignedColumns(this);
+ if (columns == null) {
+ return null;
+ }
+ accessedColumns.addAll(columns);
+ }
+ return accessedColumns;
+ }
+
@Override
/*
* Must be synchronized with sort() on the same TVList instance: a query may
sort
@@ -778,6 +1008,76 @@ public abstract class AlignedTVList extends TVList {
}
}
+ /*
+ * There are two clone modes:
+ * 1. Full clone: columnsToClone is null, meaning no column filter is
applied. All columns are
+ * deep-cloned.
+ * 2. Partial clone: columnsToClone is non-null. Columns in columnsToClone
are deep-cloned for the
+ * query that keeps using the source TVList; columns not in
columnsToClone are not copied here.
+ * They are moved from the source TVList to cloneList later, and
cloneList becomes the new
+ * working list in the memtable.
+ *
+ * This method only performs the allocation phase: copy row-level time
deletion state, clone
+ * requested value/bitmap arrays, and prepare bitmap containers that will be
needed by moved
+ * columns. It must not clear or move columns from
+ * the source TVList here. The destructive move is performed only by
PartialClonePlan.commit()
+ * after cloneList and the ownership-transfer plan are fully prepared for
publication.
+ */
+ private void cloneColumnDataTo(AlignedTVList cloneList, Set<Integer>
columnsToClone) {
+ cloneList.timeDeletedCnt = timeDeletedCnt;
+ cloneList.timeColDeletedMap = timeColDeletedMap == null ? null :
timeColDeletedMap.clone();
+
+ boolean cloneAllColumns = columnsToClone == null;
+ System.arraycopy(
+ memoryBinaryChunkSize, 0, cloneList.memoryBinaryChunkSize, 0,
dataTypes.size());
+ boolean hasBitMapsToMove = false;
+ for (int i = 0; i < values.size(); i++) {
+ // Clone value
+ List<Object> columnValues = values.get(i);
+ if (columnValues == null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_CLONE_795EB1C5,
+ i));
+ }
+ boolean shouldCloneColumn = cloneAllColumns ||
columnsToClone.contains(i);
+ if (!shouldCloneColumn) {
+ hasBitMapsToMove |= bitMaps != null && bitMaps.get(i) != null;
+ continue;
+ }
+
+ for (Object valueArray : columnValues) {
+ cloneList.values.get(i).add(cloneValue(dataTypes.get(i), valueArray));
+ }
+ // Clone bitmap in columnIndex
+ if (bitMaps != null && bitMaps.get(i) != null) {
+ List<BitMap> columnBitMaps = bitMaps.get(i);
+ if (cloneList.bitMaps == null) {
+ cloneList.bitMaps = new ArrayList<>(dataTypes.size());
+ for (int j = 0; j < dataTypes.size(); j++) {
+ cloneList.bitMaps.add(null);
+ }
+ }
+ if (cloneList.bitMaps.get(i) == null) {
+ List<BitMap> cloneColumnBitMaps = new ArrayList<>();
+ for (BitMap bitMap : columnBitMaps) {
+ cloneColumnBitMaps.add(bitMap == null ? null : bitMap.clone());
+ }
+ cloneList.bitMaps.set(i, cloneColumnBitMaps);
+ }
+ }
+ }
+ cloneList.materializedBitmapMemoryCost = materializedBitmapMemoryCost;
+
+ if (hasBitMapsToMove && cloneList.bitMaps == null) {
+ cloneList.bitMaps = new ArrayList<>(dataTypes.size());
+ for (int i = 0; i < dataTypes.size(); i++) {
+ cloneList.bitMaps.add(null);
+ }
+ }
+ }
+
@Override
protected void clearValue() {
for (int i = 0; i < dataTypes.size(); i++) {
@@ -815,7 +1115,15 @@ public abstract class AlignedTVList extends TVList {
indices.add((int[]) getPrimitiveArraysByType(TSDataType.INT32));
}
for (int i = 0; i < dataTypes.size(); i++) {
- values.get(i).add(null);
+ List<Object> columnValues = values.get(i);
+ if (columnValues == null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_EXPAND_68E0C8B6,
+ i));
+ }
+ columnValues.add(null);
if (bitMaps != null && bitMaps.get(i) != null) {
bitMaps.get(i).add(null);
materializedBitmapMemoryCost += bitmapReferenceRamCost();
@@ -1070,7 +1378,7 @@ public abstract class AlignedTVList extends TVList {
valueArray = getPrimitiveArraysByType(dataTypes.get(columnIndex));
columnValues.set(arrayIndex, valueArray);
materializedValueArrayCounts[columnIndex]++;
- materializedValueArrayMemCost +=
valueListArrayMemCost(dataTypes.get(columnIndex));
+ materializedValueArrayMemCost +=
primitiveArrayMemCost(dataTypes.get(columnIndex));
if (elementIndex > 0) {
getBitMap(columnIndex, arrayIndex).markRange(0, elementIndex);
}
@@ -1079,6 +1387,15 @@ public abstract class AlignedTVList extends TVList {
}
private BitMap getBitMap(int columnIndex, int arrayIndex) {
+ List<Object> columnValues = values.get(columnIndex);
+ if (columnValues == null) {
+ throw new IllegalStateException(
+ String.format(
+ DataNodeMiscMessages
+
.EXCEPTION_MISSING_VALUE_ARRAYS_FOR_ALIGNED_COLUMN_INDEX_ARG_DURING_MARK_NULL_VALUE_2893628E,
+ columnIndex));
+ }
+
// init BitMaps if doesn't have
if (bitMaps == null) {
List<List<BitMap>> localBitMaps = new ArrayList<>(dataTypes.size());
@@ -1090,8 +1407,8 @@ public abstract class AlignedTVList extends TVList {
// if the bitmap in columnIndex is null, init the bitmap of this column
from the beginning
if (bitMaps.get(columnIndex) == null) {
- List<BitMap> columnBitMaps = new
ArrayList<>(values.get(columnIndex).size());
- for (int i = 0; i < values.get(columnIndex).size(); i++) {
+ List<BitMap> columnBitMaps = new ArrayList<>(columnValues.size());
+ for (int i = 0; i < columnValues.size(); i++) {
columnBitMaps.add(null);
}
bitMaps.set(columnIndex, columnBitMaps);
@@ -1137,22 +1454,115 @@ public abstract class AlignedTVList extends TVList {
new ArrayList<>(dataTypes));
}
+ public synchronized RamInfo calculateRamSize(Set<Integer> columnsToClone) {
+ return new RamInfo(
+ timestamps.size(),
+ alignedTvListArrayMemCost(columnsToClone),
+ getRamSize(columnsToClone),
+ rowCount,
+ new ArrayList<>(dataTypes));
+ }
+
public synchronized long getRamSize() {
return (long) timestamps.size() *
alignedTvListArrayMemCostWithoutPrimitiveArrays()
+ materializedValueArrayMemCost
- + materializedBitmapMemoryCost;
+ + materializedBitmapMemoryCost
+ + calculateContainerRamCost(null);
+ }
+
+ public synchronized long getRamSize(Set<Integer> columnsToClone) {
+ long size =
+ (long) timestamps.size() *
alignedTvListArrayMemCostWithoutPrimitiveArrays(columnsToClone);
+ for (int i = 0; i < dataTypes.size(); i++) {
+ if (columnsToClone != null && !columnsToClone.contains(i)) {
+ continue;
+ }
+ TSDataType dataType = dataTypes.get(i);
+ if (dataType != null) {
+ size += (long) materializedValueArrayCounts[i] *
primitiveArrayMemCost(dataType);
+ }
+ }
+ return size
+ + calculateBitmapRamCost(bitMaps, columnsToClone)
+ + calculateContainerRamCost(columnsToClone);
+ }
+
+ /**
+ * Calculate the one-time container memory retained by this list.
+ *
+ * <p>The outer N-wide containers (the {@code values}/{@code bitMaps}
ArrayLists, the {@code
+ * dataTypes} list and the accounting arrays kept after a partial clone) are
not charged anywhere
+ * else and are counted in full, including all their slot references.
+ *
+ * <p>For a retained column, the inner value list is charged with the
references of all its slots
+ * (including null placeholders for blocks that were never materialized), so
each slot reference
+ * is counted exactly once; the materialized primitive array payload and
header are charged once
+ * per materialized block by {@code materializedValueArrayMemCost}. The
timestamps/indices inner
+ * lists only contribute their list object and backing-array header here,
since their primitive
+ * arrays are already charged per block by {@code
+ * alignedTvListArrayMemCostWithoutPrimitiveArrays}.
+ */
+ long calculateContainerRamCost(Set<Integer> retainedColumns) {
+ long size = 0;
+
+ size += listRamCostWithReferences(dataTypes);
+ size += RamUsageEstimator.sizeOfLongArray(memoryBinaryChunkSize.length);
+ size +=
RamUsageEstimator.sizeOfIntArray(materializedValueArrayCounts.length);
+ size += listRamCostWithoutReferences(timestamps);
+ if (indices != null) {
+ size += listRamCostWithoutReferences(indices);
+ }
+
+ size += listRamCostWithReferences(values);
+ for (int i = 0; i < values.size(); i++) {
+ if (retainedColumns != null && !retainedColumns.contains(i)) {
+ continue;
+ }
+ List<Object> columnValues = values.get(i);
+ if (columnValues != null) {
+ size += listRamCostWithReferences(columnValues);
+ }
+ }
+
+ if (bitMaps != null) {
+ size += listRamCostWithReferences(bitMaps);
+ for (int i = 0; i < bitMaps.size(); i++) {
+ if (retainedColumns != null && !retainedColumns.contains(i)) {
+ continue;
+ }
+ List<BitMap> columnBitMaps = bitMaps.get(i);
+ if (columnBitMaps != null) {
+ size += listRamCostWithoutReferences(columnBitMaps);
+ }
+ }
+ }
+ return size;
+ }
+
+ static long listRamCostWithReferences(List<?> list) {
+ return RamUsageEstimator.shallowSizeOf(list) +
RamUsageEstimator.sizeOfObjectArray(list.size());
+ }
+
+ static long listRamCostWithoutReferences(List<?> list) {
+ return RamUsageEstimator.shallowSizeOf(list)
+ + (list.isEmpty() ? 0 : RamUsageEstimator.sizeOfObjectArray(0));
}
- private static long calculateBitmapRamCost(List<List<BitMap>> bitMaps) {
+ private static long calculateBitmapRamCost(
+ List<List<BitMap>> bitMaps, Set<Integer> columnsToClone) {
if (bitMaps == null) {
return 0;
}
long size = 0;
- for (List<BitMap> columnBitMaps : bitMaps) {
+ for (int i = 0, length = bitMaps.size(); i < length; i++) {
+ if (columnsToClone != null && !columnsToClone.contains(i)) {
+ continue;
+ }
+ List<BitMap> columnBitMaps = bitMaps.get(i);
if (columnBitMaps == null) {
continue;
}
- size += (long) columnBitMaps.size() * bitmapReferenceRamCost();
+ size += columnBitMaps.size() * bitmapReferenceRamCost();
for (BitMap bitMap : columnBitMaps) {
if (bitMap != null) {
size += bitMap.ramBytesUsed();
@@ -1200,27 +1610,29 @@ public abstract class AlignedTVList extends TVList {
*
* @return AlignedTvListArrayMemSize
*/
- public long alignedTvListArrayMemCost() {
+ public long alignedTvListArrayMemCost(Set<Integer> columnsToClone) {
long size = 0;
- // value & bitmap array mem size
+ int retainedColumnNum = 0;
+ // value array mem size
for (int column = 0; column < dataTypes.size(); column++) {
+ if (columnsToClone != null && !columnsToClone.contains(column)) {
+ continue;
+ }
TSDataType type = dataTypes.get(column);
- if (type != null) {
+ if (type != null && values.get(column) != null) {
+ retainedColumnNum++;
size += (long) PrimitiveArrayManager.ARRAY_SIZE * (long)
type.getDataTypeSize();
}
}
- // size is 0 when all types are null
- if (size == 0) {
- return size;
- }
+
// time array mem size
size += PrimitiveArrayManager.ARRAY_SIZE * 8L;
// index array mem size
size += (indices != null) ? PrimitiveArrayManager.ARRAY_SIZE * 4L : 0;
// array headers mem size
- size += (long) NUM_BYTES_ARRAY_HEADER * (2 + dataTypes.size());
+ size += (long) NUM_BYTES_ARRAY_HEADER * (2 + retainedColumnNum);
// Object references size in ArrayList
- size += (long) NUM_BYTES_OBJECT_REF * (2 + dataTypes.size());
+ size += (long) NUM_BYTES_OBJECT_REF * (2 + retainedColumnNum);
return size;
}
@@ -1229,13 +1641,27 @@ public abstract class AlignedTVList extends TVList {
+ (indices != null ? (long) PrimitiveArrayManager.ARRAY_SIZE *
Integer.BYTES : 0);
}
+ private long alignedTvListArrayMemCostWithoutPrimitiveArrays(Set<Integer>
retainedColumns) {
+ long size = alignedTvListArrayMemCost(retainedColumns);
+ for (int i = 0; i < dataTypes.size(); i++) {
+ TSDataType dataType = dataTypes.get(i);
+ if (dataType != null
+ && values.get(i) != null
+ && (retainedColumns == null || retainedColumns.contains(i))) {
+ size -= valueListArrayMemCost(dataType);
+ }
+ }
+ return size;
+ }
+
private void refreshArrayMemCostWithoutPrimitiveArrays() {
long size = alignedTvListArrayMemCost();
if (indices != null) {
size -= (long) PrimitiveArrayManager.ARRAY_SIZE * Integer.BYTES;
}
- for (TSDataType dataType : dataTypes) {
- if (dataType != null) {
+ for (int i = 0; i < dataTypes.size(); i++) {
+ TSDataType dataType = dataTypes.get(i);
+ if (dataType != null && values.get(i) != null) {
size -= valueListArrayMemCost(dataType);
}
}
@@ -1255,6 +1681,10 @@ public abstract class AlignedTVList extends TVList {
return size;
}
+ public long alignedTvListArrayMemCost() {
+ return alignedTvListArrayMemCost((Set<Integer>) null);
+ }
+
/**
* Get the single column array mem cost by give type.
*
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/BackAlignedTVList.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/BackAlignedTVList.java
index 2603790bd66..c4f5174d195 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/BackAlignedTVList.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/BackAlignedTVList.java
@@ -27,7 +27,11 @@ public class BackAlignedTVList extends QuickAlignedTVList {
private final BackwardSort policy;
BackAlignedTVList(List<TSDataType> types) {
- super(types);
+ this(types, true);
+ }
+
+ BackAlignedTVList(List<TSDataType> types, boolean initializeValueColumns) {
+ super(types, initializeValueColumns);
policy = new BackwardSort(this);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/QuickAlignedTVList.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/QuickAlignedTVList.java
index c5bd5550056..9ea50a06246 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/QuickAlignedTVList.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/QuickAlignedTVList.java
@@ -26,7 +26,11 @@ public class QuickAlignedTVList extends AlignedTVList {
private final QuickSort policy;
QuickAlignedTVList(List<TSDataType> types) {
- super(types);
+ this(types, true);
+ }
+
+ QuickAlignedTVList(List<TSDataType> types, boolean initializeValueColumns) {
+ super(types, initializeValueColumns);
policy = new QuickSort(this);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
index 9dbd11cf626..a7aa2ff16e1 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TVList.java
@@ -551,6 +551,7 @@ public abstract class TVList implements WALEntryValue {
throw new UnsupportedOperationException(ERR_DATATYPE_NOT_CONSISTENT);
}
+ @TestOnly
public TVList getTvListByColumnIndex(
List<Integer> columnIndexList, List<TSDataType> dataTypeList, boolean
ignoreAllNullRows) {
throw new UnsupportedOperationException(ERR_DATATYPE_NOT_CONSISTENT);
@@ -824,6 +825,16 @@ public abstract class TVList implements WALEntryValue {
return queryContextSet;
}
+ /**
+ * Get the union of all columns accessed by queries on this TVList. For
non-AlignedTVList, returns
+ * empty set. This method should be called with queryListLock held for
thread safety.
+ *
+ * @return set of accessed column indices, or empty set if no columns are
tracked
+ */
+ public Set<Integer> getAccessedColumnsForQuery() {
+ return null;
+ }
+
public List<BitMap> getBitMap() {
return bitMap;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TimAlignedTVList.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TimAlignedTVList.java
index 61d5c75096e..c39297d59b6 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TimAlignedTVList.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/datastructure/TimAlignedTVList.java
@@ -26,7 +26,11 @@ public class TimAlignedTVList extends AlignedTVList {
private final TimSort policy;
TimAlignedTVList(List<TSDataType> types) {
- super(types);
+ this(types, true);
+ }
+
+ TimAlignedTVList(List<TSDataType> types, boolean initializeValueColumns) {
+ super(types, initializeValueColumns);
policy = new TimSort(this);
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceExecutionTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceExecutionTest.java
index e8c7994fb20..79e66f92d9e 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceExecutionTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceExecutionTest.java
@@ -24,8 +24,8 @@ import org.apache.iotdb.calc.exception.QueryProcessException;
import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.path.AlignedFullPath;
import org.apache.iotdb.commons.path.NonAlignedFullPath;
-import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
@@ -35,7 +35,7 @@ import
org.apache.iotdb.db.queryengine.execution.exchange.MPPDataExchangeManager
import org.apache.iotdb.db.queryengine.execution.exchange.sink.ISink;
import org.apache.iotdb.db.queryengine.execution.schedule.IDriverScheduler;
import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
-import org.apache.iotdb.db.storageengine.dataregion.memtable.DeviceIDFactory;
+import
org.apache.iotdb.db.storageengine.dataregion.memtable.AlignedWritableMemChunk;
import org.apache.iotdb.db.storageengine.dataregion.memtable.IMemTable;
import org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunk;
import
org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunkGroup;
@@ -49,7 +49,10 @@ import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.file.metadata.IDeviceID;
import org.apache.tsfile.file.metadata.enums.CompressionType;
import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.read.TimeValuePair;
import org.apache.tsfile.read.reader.IPointReader;
+import org.apache.tsfile.utils.TsPrimitiveType;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
import org.apache.tsfile.write.schema.MeasurementSchema;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -59,7 +62,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
@@ -69,6 +74,10 @@ import static
org.apache.iotdb.db.queryengine.common.QueryId.MOCK_QUERY_ID;
import static
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext.createFragmentInstanceContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -188,6 +197,55 @@ public class FragmentInstanceExecutionTest {
}
}
+ @Test
+ public void testTVListOwnerTransferTimeOnlyAlignedReleasesAllValueColumns()
+ throws InterruptedException {
+ ExecutorService instanceNotificationExecutor =
+ IoTDBThreadPoolFactory.newFixedThreadPool(1,
"test-instance-notification");
+ try {
+ List<IMeasurementSchema> schemas =
+ new ArrayList<>(
+ Arrays.asList(
+ new MeasurementSchema("s0", TSDataType.INT64,
TSEncoding.PLAIN),
+ new MeasurementSchema("s1", TSDataType.INT64,
TSEncoding.PLAIN),
+ new MeasurementSchema("s2", TSDataType.INT64,
TSEncoding.PLAIN)));
+ AlignedWritableMemChunk memChunk = new AlignedWritableMemChunk(schemas,
true);
+ for (int i = 0; i < 100; i++) {
+ memChunk.putAlignedRow(i, new Object[] {(long) i, (long) i * 2, (long)
i * 3});
+ }
+ AlignedTVList tvList = memChunk.getWorkingTVList();
+
+ // A table-model time-only query: tracked on the TVList with an empty
column list. The empty
+ // set must be distinguished from "untracked" so the flush-time
ownership transfer releases
+ // all value columns.
+ FragmentInstanceId id = new FragmentInstanceId(new
PlanFragmentId(MOCK_QUERY_ID, 1), "1");
+ FragmentInstanceStateMachine stateMachine =
+ new FragmentInstanceStateMachine(id, instanceNotificationExecutor);
+ FragmentInstanceContext queryContext = createFragmentInstanceContext(id,
stateMachine);
+ queryContext.addTVListToSet(ImmutableSet.of(tvList));
+ tvList.lockQueryList();
+ try {
+ tvList.getQueryContextSet().add(queryContext);
+ queryContext.putAccessedColumns(tvList, Collections.emptyList());
+ } finally {
+ tvList.unlockQueryList();
+ }
+
+ // Flush-time ownership transfer: the TVList is handed over to the only
(time-only) query.
+ memChunk.release();
+
+ // All value columns must have been released, while timestamps remain
readable.
+ assertNull(tvList.getValues().get(0));
+ assertNull(tvList.getValues().get(1));
+ assertNull(tvList.getValues().get(2));
+ assertEquals(100, tvList.rowCount());
+ assertEquals(99L, tvList.getTime(99));
+ assertSame(queryContext, tvList.getOwnerQuery());
+ } finally {
+ shutdownAndAwaitTermination(instanceNotificationExecutor);
+ }
+ }
+
private static void shutdownAndAwaitTermination(ExecutorService executor)
throws InterruptedException {
executor.shutdown();
@@ -256,6 +314,137 @@ public class FragmentInstanceExecutionTest {
}
}
+ @Test
+ public void testAlignedTVListPartialColumnCloneEndToEnd() throws
InterruptedException {
+ ExecutorService instanceNotificationExecutor =
+ IoTDBThreadPoolFactory.newFixedThreadPool(2,
"test-aligned-partial-clone");
+ try {
+ // Create an unsorted aligned MemTable with 5 columns. Row i (0..99) has
+ // value i * 100 + j in column j, and rows are inserted in reverse order.
+ List<IMeasurementSchema> schemaList = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ schemaList.add(new MeasurementSchema("sensor_" + i, TSDataType.INT64));
+ }
+ String deviceId = "d1";
+ IMemTable memTable = createMemTable(deviceId, schemaList);
+
+ assertEquals(1, memTable.getMemTableMap().size());
+ IWritableMemChunkGroup memChunkGroup =
memTable.getMemTableMap().values().iterator().next();
+ assertEquals(1, memChunkGroup.getMemChunkMap().size());
+ IWritableMemChunk memChunk =
memChunkGroup.getMemChunkMap().values().iterator().next();
+ AlignedTVList workingTvList = (AlignedTVList)
memChunk.getWorkingTVList();
+ assertFalse(workingTvList.isSorted());
+ assertEquals(100, workingTvList.rowCount());
+
+ // Two concurrent query contexts.
+ FragmentInstanceId id1 = new FragmentInstanceId(new
PlanFragmentId(MOCK_QUERY_ID, 1), "1");
+ FragmentInstanceStateMachine stateMachine1 =
+ new FragmentInstanceStateMachine(id1, instanceNotificationExecutor);
+ FragmentInstanceContext context1 = createFragmentInstanceContext(id1,
stateMachine1);
+
+ FragmentInstanceId id2 = new FragmentInstanceId(new
PlanFragmentId(MOCK_QUERY_ID, 2), "2");
+ FragmentInstanceStateMachine stateMachine2 =
+ new FragmentInstanceStateMachine(id2, instanceNotificationExecutor);
+ FragmentInstanceContext context2 = createFragmentInstanceContext(id2,
stateMachine2);
+
+ // Query 1: sensor_2 and sensor_0. It stays active on the unsorted
working TVList
+ // (no point reader is opened yet, so the list is not sorted).
+ List<String> measurements1 = Arrays.asList("sensor_2", "sensor_0");
+ List<IMeasurementSchema> schemas1 = Arrays.asList(schemaList.get(2),
schemaList.get(0));
+ AlignedFullPath fullPath1 =
+ new AlignedFullPath(
+ IDeviceID.Factory.DEFAULT_FACTORY.create(deviceId),
measurements1, schemas1);
+ ReadOnlyMemChunk readOnlyMemChunk1 =
+ memTable.query(context1, fullPath1, Long.MIN_VALUE, null, null);
+
+ // Query 2: sensor_1 and sensor_3. Because Query 1 is still active on
the unsorted
+ // working TVList, this triggers clone-and-swap of the working TVList.
+ List<String> measurements2 = Arrays.asList("sensor_1", "sensor_3");
+ List<IMeasurementSchema> schemas2 = Arrays.asList(schemaList.get(1),
schemaList.get(3));
+ AlignedFullPath fullPath2 =
+ new AlignedFullPath(
+ IDeviceID.Factory.DEFAULT_FACTORY.create(deviceId),
measurements2, schemas2);
+ ReadOnlyMemChunk readOnlyMemChunk2 =
+ memTable.query(context2, fullPath2, Long.MIN_VALUE, null, null);
+
+ // Query 1's columns (0 and 2) stay in the old working TVList; the other
columns are
+ // moved to the clone.
+ assertEquals(
+ new HashSet<>(Arrays.asList(0, 2)),
context1.getAccessedAlignedColumns(workingTvList));
+ assertNotNull(workingTvList.getValues().get(0));
+ assertNull(workingTvList.getValues().get(1));
+ assertNotNull(workingTvList.getValues().get(2));
+ assertNull(workingTvList.getValues().get(3));
+ assertNull(workingTvList.getValues().get(4));
+
+ // The memChunk now points to the clone, which owns all 5 columns.
+ AlignedTVList cloneTvList = (AlignedTVList) memChunk.getWorkingTVList();
+ assertNotSame(workingTvList, cloneTvList);
+ assertEquals(100, cloneTvList.rowCount());
+ assertNotNull(cloneTvList.getValues().get(0));
+ assertNotNull(cloneTvList.getValues().get(1));
+ assertNotNull(cloneTvList.getValues().get(2));
+ assertNotNull(cloneTvList.getValues().get(3));
+ assertNotNull(cloneTvList.getValues().get(4));
+
+ // The old working TVList is owned by Query 1, which also reserved its
memory.
+ assertSame(context1, workingTvList.getOwnerQuery());
+ assertTrue(workingTvList.getReservedMemoryBytes() > 0);
+
+ // Both queries must still read all 100 rows with correct values.
+ IPointReader pointReader1 = readOnlyMemChunk1.getPointReader();
+ IPointReader pointReader2 = readOnlyMemChunk2.getPointReader();
+ long row = 0;
+ while (pointReader1.hasNextTimeValuePair() &&
pointReader2.hasNextTimeValuePair()) {
+ TimeValuePair tvPair1 = pointReader1.nextTimeValuePair();
+ TimeValuePair tvPair2 = pointReader2.nextTimeValuePair();
+
+ assertEquals(row, tvPair1.getTimestamp());
+ assertEquals(row, tvPair2.getTimestamp());
+
+ // Query 1 reads [sensor_2, sensor_0] in query order.
+ TsPrimitiveType[] values1 = tvPair1.getValue().getVector();
+ assertEquals(2, values1.length);
+ assertEquals(row * 100 + 2, values1[0].getLong());
+ assertEquals(row * 100 + 0, values1[1].getLong());
+
+ // Query 2 reads [sensor_1, sensor_3] in query order.
+ TsPrimitiveType[] values2 = tvPair2.getValue().getVector();
+ assertEquals(2, values2.length);
+ assertEquals(row * 100 + 1, values2[0].getLong());
+ assertEquals(row * 100 + 3, values2[1].getLong());
+ row++;
+ }
+ assertEquals(100, row);
+ assertFalse(pointReader1.hasNextTimeValuePair());
+ assertFalse(pointReader2.hasNextTimeValuePair());
+
+ // Before ending the queries, the retained source's reserved memory must
equal the value
+ // recalculated at cleanup time - this is exactly the accounting
releaseTVListOwnedByQuery
+ // validates when the owning query ends (a mismatch would emit a WARN
during release).
+ long reservedBeforeRelease = workingTvList.getReservedMemoryBytes();
+ assertTrue(reservedBeforeRelease > 0);
+ assertEquals(workingTvList.calculateRamSize().getRamSize(),
reservedBeforeRelease);
+
+ // End Query 1, the owner of the retained source. No other query uses
the source any more,
+ // so the final release must return its reserved memory and clear the
list.
+ context1.releaseResource();
+ assertTrue(workingTvList.getQueryContextSet().isEmpty());
+ assertEquals(0, workingTvList.rowCount());
+
+ // End Query 2. The clone is now the memTable's working TVList, so
cleanup only detaches the
+ // query from it; the clone itself must remain intact as the working
list.
+ context2.releaseResource();
+ assertTrue(cloneTvList.getQueryContextSet().isEmpty());
+ assertEquals(100, cloneTvList.rowCount());
+ assertSame(cloneTvList, memChunk.getWorkingTVList());
+ } catch (Exception e) {
+ fail(e.getMessage());
+ } finally {
+ shutdownAndAwaitTermination(instanceNotificationExecutor);
+ }
+ }
+
private FragmentInstanceExecution createFragmentInstanceExecution(int id,
Executor executor)
throws CpuNotEnoughException {
IDriverScheduler scheduler = Mockito.mock(IDriverScheduler.class);
@@ -308,7 +497,7 @@ public class FragmentInstanceExecutionTest {
int rows = 100;
for (int i = 0; i < 100; i++) {
memTable.write(
- DeviceIDFactory.getInstance().getDeviceID(new PartialPath(deviceId)),
+ IDeviceID.Factory.DEFAULT_FACTORY.create(deviceId),
Collections.singletonList(
new MeasurementSchema(measurementId, TSDataType.INT32,
TSEncoding.PLAIN)),
rows - i - 1,
@@ -316,4 +505,21 @@ public class FragmentInstanceExecutionTest {
}
return memTable;
}
+
+ private IMemTable createMemTable(String deviceId, List<IMeasurementSchema>
schemaList)
+ throws IllegalPathException {
+ PrimitiveMemTable memTable = new PrimitiveMemTable("root.test", "1");
+
+ // Insert data in reverse order to make it unsorted
+ int rows = 100;
+ for (int i = rows - 1; i >= 0; i--) {
+ Object[] values = new Object[5];
+ for (int j = 0; j < 5; j++) {
+ values[j] = (long) i * 100 + j;
+ }
+ memTable.writeAlignedRow(
+ IDeviceID.Factory.DEFAULT_FACTORY.create(deviceId), schemaList, i,
values);
+ }
+ return memTable;
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
index 65cbc0f5b9f..14673ffda64 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
@@ -323,6 +323,11 @@ public class QueryModificationLoaderTest {
reservedBytes -= size;
}
+ @Override
+ public void releaseMemoryImmediately(long size) {
+ reservedBytes -= size;
+ }
+
@Override
public void releaseAllReservedMemory() {
reservedBytes = 0;
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlannerOperatorsMemoryTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlannerOperatorsMemoryTest.java
index 6d0cabb0443..f7362501833 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlannerOperatorsMemoryTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/LocalExecutionPlannerOperatorsMemoryTest.java
@@ -19,6 +19,7 @@
package org.apache.iotdb.db.queryengine.plan.planner;
+import org.apache.iotdb.calc.exception.MemoryNotEnoughException;
import org.apache.iotdb.db.queryengine.common.QueryId;
import
org.apache.iotdb.db.queryengine.plan.planner.memory.NotThreadSafeMemoryReservationManager;
@@ -161,4 +162,49 @@ public class LocalExecutionPlannerOperatorsMemoryTest {
Assert.assertEquals(0L, manager.getReservedBytesInTotalForTest());
Assert.assertEquals(freeBefore, PLANNER.getFreeMemoryForOperators());
}
+
+ @Test
+ public void testImmediateReservationRollback() {
+ long request = Math.min(1024L, PLANNER.getFreeMemoryForOperators());
+ if (request <= 0) {
+ return;
+ }
+
+ NotThreadSafeMemoryReservationManager manager =
+ new NotThreadSafeMemoryReservationManager(new QueryId("normal_query"),
"test");
+ long freeBefore = PLANNER.getFreeMemoryForOperators();
+
+ manager.reserveMemoryCumulatively(request);
+ manager.releaseMemoryImmediately(request);
+ manager.reserveMemoryImmediately();
+
+ Assert.assertEquals(0L, manager.getReservedBytesInTotalForTest());
+ Assert.assertEquals(freeBefore, PLANNER.getFreeMemoryForOperators());
+
+ manager.reserveMemoryImmediately(request);
+ manager.releaseMemoryImmediately(request);
+
+ Assert.assertEquals(0L, manager.getReservedBytesInTotalForTest());
+ Assert.assertEquals(freeBefore, PLANNER.getFreeMemoryForOperators());
+ }
+
+ @Test
+ public void testFailedCumulativeReservationDoesNotRemainPending() {
+ long freeBefore = PLANNER.getFreeMemoryForOperators();
+ long request = freeBefore + MEMORY_BATCH_THRESHOLD;
+ NotThreadSafeMemoryReservationManager manager =
+ new NotThreadSafeMemoryReservationManager(new QueryId("normal_query"),
"test");
+
+ try {
+ manager.reserveMemoryCumulatively(request);
+ Assert.fail("Expected insufficient query memory");
+ } catch (MemoryNotEnoughException expected) {
+ // expected
+ }
+
+ // A stale pending reservation would make this retry fail again.
+ manager.reserveMemoryImmediately();
+ Assert.assertEquals(0L, manager.getReservedBytesInTotalForTest());
+ Assert.assertEquals(freeBefore, PLANNER.getFreeMemoryForOperators());
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtilsTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtilsTest.java
new file mode 100644
index 00000000000..334e6836e79
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtilsTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.schemaengine.schemaregion.utils;
+
+import org.apache.iotdb.commons.path.NonAlignedFullPath;
+import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext;
+import org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunk;
+import org.apache.iotdb.db.utils.datastructure.TVList;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ResourceByPathUtilsTest {
+
+ @Test
+ public void testFlushingQueryLocksTemporaryTVListBeforeRegistration() throws
Exception {
+ TVList candidate = TVList.newList(TSDataType.INT64);
+ candidate.putLong(2, 2);
+ candidate.putLong(1, 1);
+ Assert.assertFalse(candidate.isSorted());
+
+ QueryContext previousQuery = new QueryContext(1, false);
+ candidate.lockQueryList();
+ try {
+ candidate.getQueryContextSet().add(previousQuery);
+ } finally {
+ candidate.unlockQueryList();
+ }
+
+ TVList temporaryList = candidate.cloneForFlushSort();
+ IWritableMemChunk memChunk = mock(IWritableMemChunk.class);
+ when(memChunk.getSortedList()).thenReturn(Collections.emptyList());
+ when(memChunk.getWorkingTVList()).thenReturn(candidate);
+ CountDownLatch temporaryListInitialized = new CountDownLatch(1);
+ when(memChunk.initWorkingListForFlushIfNecessary(candidate, true))
+ .thenAnswer(
+ ignored -> {
+ temporaryListInitialized.countDown();
+ return temporaryList;
+ });
+
+ ResourceByPathUtils resourceByPathUtils =
+ ResourceByPathUtils.getResourceInstance(
+ new NonAlignedFullPath(
+ IDeviceID.Factory.DEFAULT_FACTORY.create("root.test.d"),
+ new MeasurementSchema("s", TSDataType.INT64)));
+ QueryContext currentQuery = new QueryContext(2, false);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<Map<TVList, Integer>> result = null;
+ try {
+ temporaryList.lockQueryList();
+ try {
+ result =
+ executor.submit(
+ () ->
+ resourceByPathUtils.prepareTvListMapForQuery(
+ currentQuery, memChunk, false, null, null));
+ Assert.assertTrue(temporaryListInitialized.await(3, TimeUnit.SECONDS));
+ Future<Map<TVList, Integer>> blockedResult = result;
+ Assert.assertThrows(
+ TimeoutException.class, () -> blockedResult.get(200,
TimeUnit.MILLISECONDS));
+ } finally {
+ temporaryList.unlockQueryList();
+ }
+
+ Map<TVList, Integer> tvListQueryMap = result.get(3, TimeUnit.SECONDS);
+ Assert.assertTrue(tvListQueryMap.containsKey(temporaryList));
+ temporaryList.lockQueryList();
+ try {
+
Assert.assertTrue(temporaryList.getQueryContextSet().contains(currentQuery));
+ } finally {
+ temporaryList.unlockQueryList();
+ }
+ } finally {
+ if (result != null) {
+ result.cancel(true);
+ }
+ executor.shutdownNow();
+ executor.awaitTermination(3, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
index 43832097c0e..e1b402a64dd 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
@@ -541,21 +541,21 @@ public class TsFileProcessorTest {
true,
new long[5]);
IMemTable memTable = processor.getWorkMemTable();
- Assert.assertEquals(1596552, memTable.getTVListsRamCost());
+ Assert.assertEquals(1788704, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNode(100, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(1596552, memTable.getTVListsRamCost());
+ Assert.assertEquals(1788704, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNode(200, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(1596552, memTable.getTVListsRamCost());
+ Assert.assertEquals(1788704, memTable.getTVListsRamCost());
Assert.assertEquals(90000, memTable.getTotalPointsNum());
Assert.assertEquals(720360, memTable.memSize());
// Test records
@@ -564,7 +564,7 @@ public class TsFileProcessorTest {
record.addTuple(DataPoint.getDataPoint(dataType, measurementId,
String.valueOf(i)));
processor.insert(buildInsertRowNodeByTSRecord(record), new long[5]);
}
- Assert.assertEquals(1598168, memTable.getTVListsRamCost());
+ Assert.assertEquals(1790320, memTable.getTVListsRamCost());
Assert.assertEquals(90100, memTable.getTotalPointsNum());
Assert.assertEquals(721560, memTable.memSize());
}
@@ -589,9 +589,12 @@ public class TsFileProcessorTest {
actualProcessor.insertTablet(
genSingleMeasurementTablet(rowCount, true), rangeList, actualResults,
false, new long[5]);
+ // The failed-only block never materializes its value array, so the only
difference from the
+ // expected processor is the primitive array payload+header; the slot
reference is charged once
+ // by the container model and is identical on both sides.
Assert.assertEquals(
expectedProcessor.getWorkMemTable().getTVListsRamCost()
- - AlignedTVList.valueListArrayMemCost(dataType),
+ - AlignedTVList.primitiveArrayMemCost(dataType),
actualProcessor.getWorkMemTable().getTVListsRamCost());
Assert.assertEquals(
TSStatusCode.OUT_OF_TTL.getStatusCode(),
actualResults[failedIndex].getCode());
@@ -616,11 +619,13 @@ public class TsFileProcessorTest {
true,
new long[5]);
- long denseBlockCost =
- AlignedTVList.alignedTvListArrayMemCost(
- new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null);
+ // A new block charges the per-block cost (timestamps + headers/refs) plus
the primitive array
+ // of the newly materialized column; the column's slot reference is
charged once by the
+ // container model and is unchanged when the inner list grows by one slot
(8-byte aligned).
Assert.assertEquals(
- denseBlockCost - AlignedTVList.valueListArrayMemCost(TSDataType.INT32),
+ AlignedTVList.alignedTvListArrayMemCostWithoutPrimitiveArrays(
+ new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null)
+ + AlignedTVList.primitiveArrayMemCost(TSDataType.INT32),
processor.getWorkMemTable().getTVListsRamCost() -
ramCostBeforeNewBlock);
}
@@ -642,11 +647,13 @@ public class TsFileProcessorTest {
rowNode.setAligned(true);
processor.insert(rowNode, new long[5]);
- long denseBlockCost =
- AlignedTVList.alignedTvListArrayMemCost(
- new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null);
+ // A new block charges the per-block cost (timestamps + headers/refs) plus
the primitive array
+ // of the newly materialized column; the column's slot reference is
charged once by the
+ // container model and is unchanged when the inner list grows by one slot
(8-byte aligned).
Assert.assertEquals(
- denseBlockCost - AlignedTVList.valueListArrayMemCost(TSDataType.INT32),
+ AlignedTVList.alignedTvListArrayMemCostWithoutPrimitiveArrays(
+ new TSDataType[] {TSDataType.INT32, TSDataType.INT32}, null)
+ + AlignedTVList.primitiveArrayMemCost(TSDataType.INT32),
processor.getWorkMemTable().getTVListsRamCost() -
ramCostBeforeNewBlock);
}
@@ -778,57 +785,56 @@ public class TsFileProcessorTest {
true,
new long[5]);
IMemTable memTable = processor.getWorkMemTable();
- Assert.assertEquals(1596552, memTable.getTVListsRamCost());
+ Assert.assertEquals(1788704, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNodeFors3000ToS6000(0, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(3504552, memTable.getTVListsRamCost());
+ Assert.assertEquals(4152744, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNode(100, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(3504552, memTable.getTVListsRamCost());
+ Assert.assertEquals(4152744, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNodeFors3000ToS6000(100, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(3504552, memTable.getTVListsRamCost());
+ Assert.assertEquals(4152744, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNode(200, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(3504552, memTable.getTVListsRamCost());
+ Assert.assertEquals(4152744, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNodeFors3000ToS6000(200, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(3504552, memTable.getTVListsRamCost());
+ Assert.assertEquals(4152744, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNode(300, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(
- 5269104 - 3000L * AlignedTVList.bitmapRamCost(),
memTable.getTVListsRamCost());
+ Assert.assertEquals(5761296, memTable.getTVListsRamCost());
processor.insertTablet(
genInsertTableNodeFors3000ToS6000(300, true),
Collections.singletonList(new int[] {0, 10}),
new TSStatus[10],
true,
new long[5]);
- Assert.assertEquals(7009104, memTable.getTVListsRamCost());
+ Assert.assertEquals(7633296, memTable.getTVListsRamCost());
Assert.assertEquals(240000, memTable.getTotalPointsNum());
Assert.assertEquals(1920960, memTable.memSize());
@@ -838,14 +844,14 @@ public class TsFileProcessorTest {
record.addTuple(DataPoint.getDataPoint(dataType, measurementId,
String.valueOf(i)));
processor.insert(buildInsertRowNodeByTSRecord(record), new long[5]);
}
- Assert.assertEquals(7010720, memTable.getTVListsRamCost());
+ Assert.assertEquals(7634912, memTable.getTVListsRamCost());
// Test records
for (int i = 1; i <= 100; i++) {
TSRecord record = new TSRecord(deviceId, i);
record.addTuple(DataPoint.getDataPoint(dataType, "s1",
String.valueOf(i)));
processor.insert(buildInsertRowNodeByTSRecord(record), new long[5]);
}
- Assert.assertEquals(7012336, memTable.getTVListsRamCost());
+ Assert.assertEquals(7636528, memTable.getTVListsRamCost());
Assert.assertEquals(240200, memTable.getTotalPointsNum());
Assert.assertEquals(1923360, memTable.memSize());
}
@@ -1118,8 +1124,10 @@ public class TsFileProcessorTest {
insertAlignedRow(processor, denseDevice, PrimitiveArrayManager.ARRAY_SIZE,
true);
long denseRowRamIncrement = memTable.getTVListsRamCost() -
ramCostBeforeDenseRow;
+ // Both rows advance the same block (identical container growth), so the
difference is only the
+ // dense row's materialized value array payload+header, not its slot
reference.
Assert.assertEquals(
- AlignedTVList.valueListArrayMemCost(dataType),
+ AlignedTVList.primitiveArrayMemCost(dataType),
denseRowRamIncrement - sparseRowRamIncrement);
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
index fbbd35cff43..aceb2e23420 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/datastructure/AlignedTVListTest.java
@@ -36,7 +36,10 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import static
org.apache.iotdb.db.storageengine.rescon.memory.PrimitiveArrayManager.ARRAY_SIZE;
import static org.apache.tsfile.utils.RamUsageEstimator.NUM_BYTES_ARRAY_HEADER;
@@ -175,6 +178,7 @@ public class AlignedTVListTest {
}
Assert.assertNull(tvList.getBitMaps());
+ long ramSizeBeforeBitmapAllocation =
tvList.calculateRamSize().getRamSize();
tvList.putAlignedValue(ARRAY_SIZE * 2 + 1L, new Object[] {null, 1L});
List<BitMap> firstColumnBitMaps = tvList.getBitMaps().get(0);
@@ -188,9 +192,14 @@ public class AlignedTVListTest {
firstColumnBitMaps.get(2).ramBytesUsed() < new
BitMap(ARRAY_SIZE).ramBytesUsed());
Assert.assertTrue(tvList.isNullValue(ARRAY_SIZE * 2 + 1, 0));
Assert.assertFalse(tvList.isNullValue(ARRAY_SIZE * 2, 0));
+ // Only the lazily allocated bitmap, its slot references and the container
of the newly
+ // created bitmap structure are charged; no additional value array is
materialized.
Assert.assertEquals(
- 3L * AlignedTVList.bitmapReferenceRamCost() +
AlignedTVList.bitmapRamCost(),
- tvList.calculateRamSize().getRamSize() - 3L *
tvList.alignedTvListArrayMemCost());
+ 3L * AlignedTVList.bitmapReferenceRamCost()
+ + AlignedTVList.bitmapRamCost()
+ + AlignedTVList.listRamCostWithReferences(tvList.getBitMaps())
+ +
AlignedTVList.listRamCostWithoutReferences(tvList.getBitMaps().get(0)),
+ tvList.calculateRamSize().getRamSize() -
ramSizeBeforeBitmapAllocation);
}
@Test
@@ -266,12 +275,17 @@ public class AlignedTVListTest {
Assert.assertEquals(1, tvList.getLongByValueIndex(ARRAY_SIZE + 1, 1));
long ramSizeBeforeExtension = tvList.calculateRamSize().getRamSize();
+ long containerBeforeExtension = tvList.calculateContainerRamCost(null);
tvList.extendColumn(TSDataType.INT32);
Assert.assertNull(tvList.getValues().get(2).get(0));
Assert.assertNull(tvList.getValues().get(2).get(1));
Assert.assertNull(tvList.getBitMaps().get(2));
- Assert.assertEquals(ramSizeBeforeExtension,
tvList.calculateRamSize().getRamSize());
+ // extendColumn only adds the N-wide container overhead of the new column;
neither a value
+ // array nor a bitmap is materialized.
+ Assert.assertEquals(
+ ramSizeBeforeExtension + tvList.calculateContainerRamCost(null) -
containerBeforeExtension,
+ tvList.calculateRamSize().getRamSize());
long ramSizeBeforeExtendedColumnMaterialization =
tvList.calculateRamSize().getRamSize();
tvList.putAlignedValue(ARRAY_SIZE + 2L, new Object[] {null, null, 2});
@@ -286,10 +300,13 @@ public class AlignedTVListTest {
Assert.assertTrue(tvList.isNullValue(0, 2));
Assert.assertFalse(tvList.isNullValue(ARRAY_SIZE + 2, 2));
Assert.assertEquals(2, tvList.getIntByValueIndex(ARRAY_SIZE + 2, 2));
+ // Materializing the extended column charges its primitive array, the
lazily created bitmap
+ // (slot references + bitmap) and the container of the new bitmap
structure.
Assert.assertEquals(
- AlignedTVList.valueListArrayMemCost(TSDataType.INT32)
+ AlignedTVList.primitiveArrayMemCost(TSDataType.INT32)
+ 2L * AlignedTVList.bitmapReferenceRamCost()
- + AlignedTVList.bitmapRamCost(),
+ + AlignedTVList.bitmapRamCost()
+ +
AlignedTVList.listRamCostWithoutReferences(tvList.getBitMaps().get(2)),
tvList.calculateRamSize().getRamSize() -
ramSizeBeforeExtendedColumnMaterialization);
}
@@ -304,10 +321,14 @@ public class AlignedTVListTest {
long ramSizeBeforeMaterialization = tvList.calculateRamSize().getRamSize();
tvList.putAlignedValue(ARRAY_SIZE + 1L, new Object[] {1L, 1L});
+ // Materializing the second column charges its primitive array, the lazily
created bitmap
+ // (slot references + bitmap) and the container of the new bitmap
structure.
Assert.assertEquals(
- AlignedTVList.valueListArrayMemCost(TSDataType.INT64)
+ AlignedTVList.primitiveArrayMemCost(TSDataType.INT64)
+ 2L * AlignedTVList.bitmapReferenceRamCost()
- + AlignedTVList.bitmapRamCost(),
+ + AlignedTVList.bitmapRamCost()
+ + AlignedTVList.listRamCostWithReferences(tvList.getBitMaps())
+ +
AlignedTVList.listRamCostWithoutReferences(tvList.getBitMaps().get(1)),
tvList.calculateRamSize().getRamSize() - ramSizeBeforeMaterialization);
Assert.assertEquals(
@@ -321,14 +342,18 @@ public class AlignedTVListTest {
Assert.assertEquals(
(long) projectedTvList.getValues().get(0).size()
*
projectedTvList.alignedTvListArrayMemCostWithoutPrimitiveArrays()
- + AlignedTVList.valueListArrayMemCost(TSDataType.INT64)
+ + AlignedTVList.primitiveArrayMemCost(TSDataType.INT64)
+ (long) projectedTvList.getBitMaps().get(0).size()
* AlignedTVList.bitmapReferenceRamCost()
- + AlignedTVList.bitmapRamCost(),
+ + AlignedTVList.bitmapRamCost()
+ + projectedTvList.calculateContainerRamCost(null),
projectedTvList.calculateRamSize().getRamSize());
tvList.clear();
- Assert.assertEquals(0, tvList.calculateRamSize().getRamSize());
+ // clear() keeps the N-wide containers for reuse, so only the retained
(empty) container
+ // baseline remains charged; no per-block, materialized-array or bitmap
payload is left.
+ Assert.assertEquals(
+ tvList.calculateContainerRamCost(null),
tvList.calculateRamSize().getRamSize());
}
@Test
@@ -340,9 +365,12 @@ public class AlignedTVListTest {
}
int blockCount = tvList.getValues().get(0).size();
- long denseRamSize = blockCount * tvList.alignedTvListArrayMemCost();
+ // The second column is never materialized (only nulls were written), so
only the first
+ // column's primitive arrays are charged; the N-wide container baseline is
added once.
long expectedRamSize =
- denseRamSize - blockCount *
AlignedTVList.valueListArrayMemCost(TSDataType.INT64);
+ (long) blockCount *
tvList.alignedTvListArrayMemCostWithoutPrimitiveArrays()
+ + (long) blockCount *
AlignedTVList.primitiveArrayMemCost(TSDataType.INT64)
+ + tvList.calculateContainerRamCost(null);
Assert.assertEquals(expectedRamSize,
tvList.calculateRamSize().getRamSize());
Assert.assertNull(tvList.getBitMaps());
@@ -586,4 +614,169 @@ public class AlignedTVListTest {
Assert.assertEquals(tvList.memoryBinaryChunkSize[0], 0);
Assert.assertEquals(tvList.memoryBinaryChunkSize[1], 0);
}
+
+ @Test
+ public void testMovesUnclonedColumns() {
+ List<TSDataType> dataTypes = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ dataTypes.add(TSDataType.INT64);
+ }
+ AlignedTVList tvList = AlignedTVList.newAlignedList(dataTypes);
+ tvList.putAlignedValue(0, new Object[] {1L, 2L, null});
+
+ Set<Integer> columnsToClone = Collections.singleton(1);
+ long retainedRamSize =
tvList.calculateRamSize(columnsToClone).getRamSize();
+ AlignedTVList.PartialClonePlan partialClonePlan =
tvList.preparePartialClone(columnsToClone);
+ AlignedTVList clonedTvList = partialClonePlan.getCloneList();
+
+ Assert.assertNotNull(tvList.getValues().get(0));
+ Assert.assertNotNull(tvList.getValues().get(2));
+ Assert.assertEquals(1L, tvList.getLongByValueIndex(0, 0));
+ Assert.assertTrue(tvList.isNullValue(0, 2));
+ Assert.assertEquals(2L, clonedTvList.getLongByValueIndex(0, 1));
+
+ partialClonePlan.commit();
+
+ Assert.assertNull(tvList.getValues().get(0));
+ Assert.assertNull(tvList.getValues().get(2));
+ Assert.assertTrue(tvList.isNullValue(0, 0));
+ Assert.assertTrue(tvList.isNullValue(0, 2));
+ Assert.assertEquals(1L, clonedTvList.getLongByValueIndex(0, 0));
+ Assert.assertEquals(2L, clonedTvList.getLongByValueIndex(0, 1));
+ Assert.assertTrue(clonedTvList.isNullValue(0, 2));
+ Assert.assertEquals(retainedRamSize,
tvList.calculateRamSize().getRamSize());
+ }
+
+ @Test
+ public void testPartialRamSizeScalesWithRetainedColumns() {
+ int columnCount = 256;
+ List<TSDataType> dataTypes = new ArrayList<>(columnCount);
+ Object[] values = new Object[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ dataTypes.add(TSDataType.INT64);
+ values[i] = (long) i;
+ }
+
+ AlignedTVList tvList = AlignedTVList.newAlignedList(dataTypes);
+ tvList.putAlignedValue(1, values);
+ Set<Integer> retainedColumns = Collections.singleton(0);
+ long retainedRamSize =
tvList.calculateRamSize(retainedColumns).getRamSize();
+ long fullRamSize = tvList.calculateRamSize().getRamSize();
+
+ // The N-wide container baseline is retained regardless of how many
columns are cloned, so
+ // exclude it to verify that the per-column payload scales with the
retained column count:
+ // keeping 1 of 256 columns must cost far less than the full list.
+ long retainedPayload = retainedRamSize -
tvList.calculateContainerRamCost(retainedColumns);
+ long fullPayload = fullRamSize - tvList.calculateContainerRamCost(null);
+ Assert.assertTrue(retainedPayload < fullPayload / 64);
+
+ AlignedTVList.PartialClonePlan plan =
tvList.preparePartialClone(retainedColumns);
+ plan.commit();
+ Assert.assertEquals(retainedRamSize,
tvList.calculateRamSize().getRamSize());
+ }
+
+ @Test
+ public void testPartialReservationMatchesCleanupCalculation() {
+ for (boolean createIndices : new boolean[] {false, true}) {
+ for (boolean retainValueColumn : new boolean[] {false, true}) {
+ AlignedTVList tvList =
+ AlignedTVList.newAlignedList(
+ new ArrayList<>(
+ Arrays.asList(TSDataType.INT64, TSDataType.INT64,
TSDataType.INT64)));
+ for (int i = 0; i <= ARRAY_SIZE; i++) {
+ long time = createIndices ? ARRAY_SIZE - i : i;
+ tvList.putAlignedValue(
+ time, new Object[] {(long) i, i % 2 == 0 ? null : (long) i,
(long) i});
+ }
+ if (createIndices) {
+ Assert.assertFalse(tvList.isSorted());
+ tvList.sort();
+ Assert.assertNotNull(tvList.getIndices());
+ } else {
+ Assert.assertNull(tvList.getIndices());
+ }
+
+ Set<Integer> retainedColumns =
+ retainValueColumn ? Collections.singleton(1) :
Collections.emptySet();
+ long reservedMemoryBytes =
tvList.calculateRamSize(retainedColumns).getRamSize();
+ tvList.setReservedMemoryBytes(reservedMemoryBytes);
+
+ AlignedTVList.PartialClonePlan plan =
tvList.preparePartialClone(retainedColumns);
+ plan.commit();
+
+ long cleanupMemoryBytes = tvList.calculateRamSize().getRamSize();
+ String scenario =
+ String.format(
+ "createIndices=%s, retainValueColumn=%s", createIndices,
retainValueColumn);
+ Assert.assertEquals(scenario, reservedMemoryBytes, cleanupMemoryBytes);
+ Assert.assertEquals(scenario, tvList.getReservedMemoryBytes(),
cleanupMemoryBytes);
+ }
+ }
+ }
+
+ @Test
+ public void testPartialCloneFailureLeavesSourceUntouched() {
+ AlignedTVList tvList =
+ AlignedTVList.newAlignedList(
+ Arrays.asList(TSDataType.INT64, TSDataType.INT64,
TSDataType.INT64));
+ // Materialize the first column before writing a null so master creates
its lazy bitmap.
+ tvList.putAlignedValue(0, new Object[] {1L, 2L, 3L});
+ tvList.putAlignedValue(1, new Object[] {null, 4L, 5L});
+
+ List<Object> firstColumnValues = tvList.getValues().get(0);
+ List<Object> secondColumnValues = tvList.getValues().get(1);
+ List<Object> thirdColumnValues = tvList.getValues().get(2);
+ List<BitMap> firstColumnBitMaps = tvList.getBitMaps().get(0);
+ Object invalidThirdColumnArray = new int[ARRAY_SIZE];
+ thirdColumnValues.set(0, invalidThirdColumnArray);
+
+ Set<Integer> columnsToClone = new HashSet<>(Arrays.asList(0, 1, 2));
+ Assert.assertThrows(ClassCastException.class, () ->
tvList.preparePartialClone(columnsToClone));
+
+ Assert.assertSame(firstColumnValues, tvList.getValues().get(0));
+ Assert.assertSame(secondColumnValues, tvList.getValues().get(1));
+ Assert.assertSame(thirdColumnValues, tvList.getValues().get(2));
+ Assert.assertSame(invalidThirdColumnArray,
tvList.getValues().get(2).get(0));
+ Assert.assertSame(firstColumnBitMaps, tvList.getBitMaps().get(0));
+ Assert.assertTrue(tvList.isNullValue(1, 0));
+ Assert.assertEquals(2L, tvList.getLongByValueIndex(0, 1));
+ }
+
+ @Test
+ public void testReleaseNonQueryColumnsWithBitmaps() {
+ List<TSDataType> dataTypes = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ dataTypes.add(TSDataType.INT64);
+ }
+ AlignedTVList tvList = AlignedTVList.newAlignedList(dataTypes);
+ for (int i = 0; i < 100; i++) {
+ Object[] values = new Object[3];
+ values[0] = (long) i;
+ // Alternate non-null and null values so the lazily allocated value
array and bitmap both
+ // exist in every block.
+ values[1] = i % 2 == 0 ? (long) i : null;
+ values[2] = (long) (i * 100);
+ tvList.putAlignedValue(i, values);
+ }
+
+ // Verify bitmaps were created for column 1
+ Assert.assertNotNull(tvList.getBitMaps());
+ Assert.assertNotNull(tvList.getBitMaps().get(1));
+
+ // Keep only column 0 and 2, release column 1
+ Set<Integer> columnsToKeep = new HashSet<>(Arrays.asList(0, 2));
+ tvList.releaseNonQueryColumns(columnsToKeep);
+
+ // Verify column 1 is released
+ Assert.assertNull(tvList.getValues().get(1));
+ Assert.assertNull(tvList.getBitMaps().get(1));
+
+ // Verify columns 0 and 2 are intact
+ Assert.assertFalse(tvList.getValues().get(0).isEmpty());
+ Assert.assertFalse(tvList.getValues().get(2).isEmpty());
+ for (int i = 0; i < 100; i++) {
+ Assert.assertEquals((long) i, tvList.getLongByValueIndex(i, 0));
+ Assert.assertEquals((long) (i * 100), tvList.getLongByValueIndex(i, 2));
+ }
+ }
}