This is an automated email from the ASF dual-hosted git repository.
jt2594838 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 37cf3cac19c [Performance] Reduce temporary allocations on insert path
(#18240)
37cf3cac19c is described below
commit 37cf3cac19c4f5aca053bd14b41cdaa3618360f1
Author: Caideyipi <[email protected]>
AuthorDate: Mon Jul 20 10:19:29 2026 +0800
[Performance] Reduce temporary allocations on insert path (#18240)
* Reduce temporary allocations on insert path
* Add opt-in write path performance benchmarks
---
.../resource/memory/InsertNodeMemoryEstimator.java | 99 +++++---
.../planner/plan/node/write/InsertRowNode.java | 29 ++-
.../plan/node/write/RelationalInsertRowNode.java | 7 +-
.../fetcher/cache/LastCacheUpdateSource.java | 37 +++
.../fetcher/cache/TableDeviceCacheEntry.java | 35 ++-
.../fetcher/cache/TableDeviceLastCache.java | 124 +++++++---
.../fetcher/cache/TableDeviceSchemaCache.java | 36 ++-
.../cache/TreeDeviceSchemaCacheManager.java | 11 +
.../cache/TreeDeviceSchemaCacheManagerTest.java | 25 +-
.../InsertNodeMemoryEstimatorPerformanceTest.java | 175 ++++++++++++++
.../cache/TableDeviceLastCachePerformanceTest.java | 265 +++++++++++++++++++++
.../fetcher/cache/TableDeviceLastCacheTest.java | 34 +++
.../iotdb/db/utils/ManualPerformanceTestUtils.java | 186 +++++++++++++++
13 files changed, 963 insertions(+), 100 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimator.java
index b7b660f638c..f4c9442ce01 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimator.java
@@ -28,6 +28,7 @@ import org.apache.iotdb.commons.consensus.index.ProgressIndex;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.commons.utils.TestOnly;
import org.apache.iotdb.db.i18n.DataNodePipeMessages;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertMultiTabletsNode;
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode;
@@ -155,6 +156,12 @@ public class InsertNodeMemoryEstimator {
// from the actual result because the properties of the parent class are not
added.
private static final double INSERT_ROW_NODE_EXPANSION_FACTOR = 1.3;
+ // Composite insert nodes are estimated on write threads. Reuse the identity
set between events,
+ // but discard unusually large sets to avoid retaining a large table on
every write thread.
+ private static final int MAX_RETAINED_DEDUPLICATED_OBJECTS = 1024;
+ private static final ThreadLocal<Set<Object>> REUSABLE_DEDUPLICATED_OBJECTS =
+
ThreadLocal.withInitial(InsertNodeMemoryEstimator::newDeduplicatedObjectSet);
+
public static long sizeOf(final InsertNode insertNode) {
try {
final String className = insertNode.getClass().getSimpleName();
@@ -248,46 +255,62 @@ public class InsertNodeMemoryEstimator {
private static long sizeOfInsertRowsNode(final InsertRowsNode node) {
final Set<Object> deduplicatedObjects =
- newDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
- long size = INSERT_ROWS_NODE_SIZE;
- size += calculateFullInsertNodeSize(node, deduplicatedObjects);
- size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
- size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
- size += sizeOfResults(node.getResults());
- return size;
+ acquireDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
+ try {
+ long size = INSERT_ROWS_NODE_SIZE;
+ size += calculateFullInsertNodeSize(node, deduplicatedObjects);
+ size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
+ size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
+ size += sizeOfResults(node.getResults());
+ return size;
+ } finally {
+ releaseDeduplicatedObjectSet(deduplicatedObjects);
+ }
}
private static long sizeOfInsertRowsOfOneDeviceNode(final
InsertRowsOfOneDeviceNode node) {
final Set<Object> deduplicatedObjects =
- newDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
- long size = INSERT_ROWS_OF_ONE_DEVICE_NODE_SIZE;
- size += calculateFullInsertNodeSize(node, deduplicatedObjects);
- size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
- size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
- size += sizeOfResults(node.getResults());
- return size;
+ acquireDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
+ try {
+ long size = INSERT_ROWS_OF_ONE_DEVICE_NODE_SIZE;
+ size += calculateFullInsertNodeSize(node, deduplicatedObjects);
+ size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
+ size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
+ size += sizeOfResults(node.getResults());
+ return size;
+ } finally {
+ releaseDeduplicatedObjectSet(deduplicatedObjects);
+ }
}
private static long sizeOfInsertMultiTabletsNode(final
InsertMultiTabletsNode node) {
final Set<Object> deduplicatedObjects =
- newDeduplicatedObjectSetIfNeeded(node.getInsertTabletNodeList());
- long size = INSERT_MULTI_TABLETS_NODE_SIZE;
- size += calculateFullInsertNodeSize(node, deduplicatedObjects);
- size += sizeOfInsertTabletNodeList(node.getInsertTabletNodeList(),
deduplicatedObjects);
- size += sizeOfIntegerList(node.getParentInsertTabletNodeIndexList());
- size += sizeOfResults(node.getResults());
- return size;
+ acquireDeduplicatedObjectSetIfNeeded(node.getInsertTabletNodeList());
+ try {
+ long size = INSERT_MULTI_TABLETS_NODE_SIZE;
+ size += calculateFullInsertNodeSize(node, deduplicatedObjects);
+ size += sizeOfInsertTabletNodeList(node.getInsertTabletNodeList(),
deduplicatedObjects);
+ size += sizeOfIntegerList(node.getParentInsertTabletNodeIndexList());
+ size += sizeOfResults(node.getResults());
+ return size;
+ } finally {
+ releaseDeduplicatedObjectSet(deduplicatedObjects);
+ }
}
private static long sizeOfRelationalInsertRowsNode(final
RelationalInsertRowsNode node) {
final Set<Object> deduplicatedObjects =
- newDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
- long size = RELATIONAL_INSERT_ROWS_NODE_SIZE;
- size += calculateFullInsertNodeSize(node, deduplicatedObjects);
- size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
- size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
- // ignore deviceIDs
- return size;
+ acquireDeduplicatedObjectSetIfNeeded(node.getInsertRowNodeList());
+ try {
+ long size = RELATIONAL_INSERT_ROWS_NODE_SIZE;
+ size += calculateFullInsertNodeSize(node, deduplicatedObjects);
+ size += sizeOfInsertRowNodeList(node.getInsertRowNodeList(),
deduplicatedObjects);
+ size += sizeOfIntegerList(node.getInsertRowNodeIndexList());
+ // ignore deviceIDs
+ return size;
+ } finally {
+ releaseDeduplicatedObjectSet(deduplicatedObjects);
+ }
}
private static long sizeOfRelationalInsertRowNode(final
RelationalInsertRowNode node) {
@@ -775,8 +798,24 @@ public class InsertNodeMemoryEstimator {
return Collections.newSetFromMap(new IdentityHashMap<>());
}
- private static Set<Object> newDeduplicatedObjectSetIfNeeded(final List<?>
children) {
- return children != null && children.size() > 1 ?
newDeduplicatedObjectSet() : null;
+ private static Set<Object> acquireDeduplicatedObjectSetIfNeeded(final
List<?> children) {
+ return children != null && children.size() > 1 ?
REUSABLE_DEDUPLICATED_OBJECTS.get() : null;
+ }
+
+ private static void releaseDeduplicatedObjectSet(final Set<Object>
deduplicatedObjects) {
+ if (deduplicatedObjects == null) {
+ return;
+ }
+ final boolean oversized = deduplicatedObjects.size() >
MAX_RETAINED_DEDUPLICATED_OBJECTS;
+ deduplicatedObjects.clear();
+ if (oversized) {
+ REUSABLE_DEDUPLICATED_OBJECTS.remove();
+ }
+ }
+
+ @TestOnly
+ static void clearReusableDeduplicatedObjectsForTest() {
+ REUSABLE_DEDUPLICATED_OBJECTS.remove();
}
private static boolean shouldCountObject(
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertRowNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertRowNode.java
index 04768c57b50..5a3628ac06c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertRowNode.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertRowNode.java
@@ -33,6 +33,7 @@ import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.WritePlanNode;
+import
org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.LastCacheUpdateSource;
import
org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.TreeDeviceSchemaCacheManager;
import org.apache.iotdb.db.storageengine.dataregion.memtable.AbstractMemTable;
import
org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunkGroup;
@@ -60,7 +61,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
-public class InsertRowNode extends InsertNode implements WALEntryValue {
+public class InsertRowNode extends InsertNode implements WALEntryValue,
LastCacheUpdateSource {
private static final byte TYPE_RAW_STRING = -1;
@@ -1010,6 +1011,21 @@ public class InsertRowNode extends InsertNode implements
WALEntryValue {
return new TimeValuePair(time,
TsPrimitiveType.getByType(dataTypes[columnIndex], value));
}
+ @Override
+ public long getLastCacheTimestamp() {
+ return time;
+ }
+
+ @Override
+ public boolean hasLastCacheValue(final int index) {
+ return canComposeTimeValuePair(index);
+ }
+
+ @Override
+ public TimeValuePair getLastCacheValue(final int index) {
+ return composeTimeValuePair(index);
+ }
+
private boolean canComposeTimeValuePair(final int columnIndex) {
return measurements != null
&& columnIndex >= 0
@@ -1026,18 +1042,9 @@ public class InsertRowNode extends InsertNode implements
WALEntryValue {
}
public void updateLastCache(String databaseName) {
- TimeValuePair[] timeValuePairs = new TimeValuePair[measurements.length];
- for (int i = 0; i < measurements.length; i++) {
- timeValuePairs[i] = composeTimeValuePair(i);
- }
TreeDeviceSchemaCacheManager.getInstance()
.updateLastCacheIfExists(
- databaseName,
- getDeviceID(),
- measurements,
- timeValuePairs,
- isAligned,
- measurementSchemas);
+ databaseName, getDeviceID(), measurements, this, isAligned,
measurementSchemas);
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/RelationalInsertRowNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/RelationalInsertRowNode.java
index b11c0f6784e..f0d0d8de7d6 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/RelationalInsertRowNode.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/RelationalInsertRowNode.java
@@ -35,7 +35,6 @@ import
org.apache.iotdb.db.storageengine.dataregion.wal.buffer.IWALByteBufferVie
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.file.metadata.IDeviceID;
import org.apache.tsfile.file.metadata.IDeviceID.Factory;
-import org.apache.tsfile.read.TimeValuePair;
import org.apache.tsfile.utils.ReadWriteIOUtils;
import org.apache.tsfile.write.schema.MeasurementSchema;
@@ -281,13 +280,9 @@ public class RelationalInsertRowNode extends InsertRowNode
{
@Override
public void updateLastCache(String databaseName) {
- TimeValuePair[] timeValuePairs = new TimeValuePair[measurements.length];
- for (int i = 0; i < measurements.length; i++) {
- timeValuePairs[i] = composeTimeValuePair(i);
- }
TableDeviceSchemaCache.getInstance()
.updateLastCacheIfExists(
- databaseName, getDeviceID(), measurements, measurementSchemas,
timeValuePairs);
+ databaseName, getDeviceID(), measurements, measurementSchemas,
this);
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/LastCacheUpdateSource.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/LastCacheUpdateSource.java
new file mode 100644
index 00000000000..2cd45460024
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/LastCacheUpdateSource.java
@@ -0,0 +1,37 @@
+/*
+ * 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.queryengine.plan.relational.metadata.fetcher.cache;
+
+import org.apache.tsfile.read.TimeValuePair;
+
+/**
+ * Provides row values lazily when updating last cache on the write path.
+ *
+ * <p>{@link #getLastCacheValue(int)} is called only when the corresponding
cache entry exists and
+ * its timestamp is eligible for update.
+ */
+public interface LastCacheUpdateSource {
+
+ long getLastCacheTimestamp();
+
+ boolean hasLastCacheValue(int index);
+
+ TimeValuePair getLastCacheValue(int index);
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceCacheEntry.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceCacheEntry.java
index 5ea51a5809f..394cd509571 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceCacheEntry.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceCacheEntry.java
@@ -140,13 +140,22 @@ public class TableDeviceCacheEntry {
return 0;
}
// Safe here because tree schema is invalidated by the whole entry
- final int result =
- (deviceSchema.compareAndSet(null, new TreeDeviceNormalSchema(database,
isAligned))
- ? TreeDeviceNormalSchema.INSTANCE_SIZE
- : 0);
- return deviceSchema.get() instanceof TreeDeviceNormalSchema
- ? result + ((TreeDeviceNormalSchema)
deviceSchema.get()).update(measurements, schemas)
- : 0;
+ IDeviceSchema schema = deviceSchema.get();
+ int result = 0;
+ if (schema == null) {
+ final TreeDeviceNormalSchema newSchema = new
TreeDeviceNormalSchema(database, isAligned);
+ if (deviceSchema.compareAndSet(null, newSchema)) {
+ schema = newSchema;
+ result = TreeDeviceNormalSchema.INSTANCE_SIZE;
+ } else {
+ schema = deviceSchema.get();
+ }
+ }
+ if (!(schema instanceof TreeDeviceNormalSchema)) {
+ return 0;
+ }
+ result += ((TreeDeviceNormalSchema) schema).update(measurements, schemas);
+ return deviceSchema.get() == schema ? result : 0;
}
IDeviceSchema getDeviceSchema() {
@@ -203,6 +212,18 @@ public class TableDeviceCacheEntry {
return Objects.nonNull(lastCache.get()) ? result : 0;
}
+ int tryUpdateLastCache(
+ final String[] measurements,
+ final @Nullable IMeasurementSchema[] measurementSchemas,
+ final LastCacheUpdateSource updateSource) {
+ final TableDeviceLastCache cache = lastCache.get();
+ final int result =
+ Objects.nonNull(cache)
+ ? cache.tryUpdate(measurements, measurementSchemas, updateSource)
+ : 0;
+ return Objects.nonNull(lastCache.get()) ? result : 0;
+ }
+
int tryUpdateLastCache(final String[] measurements, final TimeValuePair[]
timeValuePairs) {
return tryUpdateLastCache(measurements, timeValuePairs, false);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCache.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCache.java
index 325484f6f68..5c79457bada 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCache.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCache.java
@@ -33,11 +33,11 @@ import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -73,8 +73,9 @@ public class TableDeviceLastCache {
new TimeValuePair(Long.MIN_VALUE, PLACEHOLDER_NO_VALUE);
// Time is seen as "" as a measurement
- private final Map<String, TimeValuePair> measurement2CachedLastMap = new
ConcurrentHashMap<>();
- private final Map<String, Long> measurement2CachedLastKnownNullTimeMap =
+ private final ConcurrentMap<String, TimeValuePair> measurement2CachedLastMap
=
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, Long>
measurement2CachedLastKnownNullTimeMap =
new ConcurrentHashMap<>();
private final boolean isTableModel;
@@ -143,7 +144,7 @@ public class TableDeviceLastCache {
final @Nullable IMeasurementSchema[] measurementSchemas,
final @Nonnull TimeValuePair[] timeValuePairs,
final boolean invalidateNull) {
- final AtomicInteger diff = new AtomicInteger(0);
+ int diff = 0;
long lastTime = Long.MIN_VALUE;
for (int i = 0; i < measurements.length; ++i) {
@@ -151,49 +152,93 @@ public class TableDeviceLastCache {
if (Objects.isNull(measurement)) {
continue;
}
- if (Objects.isNull(timeValuePairs[i])) {
+ final TimeValuePair timeValuePair = timeValuePairs[i];
+ if (Objects.isNull(timeValuePair)) {
if (invalidateNull) {
- diff.addAndGet(removeKnownNullTime(measurement));
- diff.addAndGet(
- -((int) RamUsageEstimator.sizeOf(measurement)
- +
getTvPairEntrySize(measurement2CachedLastMap.remove(measurement))));
+ diff += removeKnownNullTime(measurement);
+ diff -=
+ (int) RamUsageEstimator.sizeOf(measurement)
+ +
getTvPairEntrySize(measurement2CachedLastMap.remove(measurement));
}
continue;
}
- if (isKnownNullAtAlignedTime(measurement, timeValuePairs[i])) {
- if (lastTime < timeValuePairs[i].getTimestamp()) {
- lastTime = timeValuePairs[i].getTimestamp();
- }
- diff.addAndGet(tryUpdateKnownNullTime(measurement,
timeValuePairs[i].getTimestamp()));
+ if (lastTime < timeValuePair.getTimestamp()) {
+ lastTime = timeValuePair.getTimestamp();
+ }
+ if (isKnownNullAtAlignedTime(measurement, timeValuePair)) {
+ diff += tryUpdateKnownNullTime(measurement,
timeValuePair.getTimestamp());
+ } else {
+ diff += tryUpdateCachedLast(measurement, timeValuePair);
+ }
+ }
+ tryUpdateLastTime(lastTime);
+ return diff;
+ }
+
+ int tryUpdate(
+ final @Nonnull String[] measurements,
+ final @Nullable IMeasurementSchema[] measurementSchemas,
+ final LastCacheUpdateSource updateSource) {
+ int diff = 0;
+ boolean hasValue = false;
+
+ for (int i = 0; i < measurements.length; ++i) {
+ final String measurement = getRawMeasurement(measurements,
measurementSchemas, i);
+ if (Objects.isNull(measurement) || !updateSource.hasLastCacheValue(i)) {
continue;
}
+ hasValue = true;
+ diff += tryUpdateCachedLast(measurement, updateSource, i);
+ }
+ if (hasValue) {
+ tryUpdateLastTime(updateSource.getLastCacheTimestamp());
+ }
+ return diff;
+ }
- final int finalI = i;
- if (lastTime < timeValuePairs[i].getTimestamp()) {
- lastTime = timeValuePairs[i].getTimestamp();
+ private int tryUpdateCachedLast(
+ final String measurement, final LastCacheUpdateSource updateSource,
final int index) {
+ final TimeValuePair cachedPair =
measurement2CachedLastMap.get(measurement);
+ if (Objects.isNull(cachedPair)
+ || cachedPair.getTimestamp() > updateSource.getLastCacheTimestamp()) {
+ return 0;
+ }
+ final TimeValuePair newPair = updateSource.getLastCacheValue(index);
+ return Objects.nonNull(newPair) ? tryUpdateCachedLast(measurement,
cachedPair, newPair) : 0;
+ }
+
+ private int tryUpdateCachedLast(final String measurement, final
TimeValuePair newPair) {
+ return tryUpdateCachedLast(measurement,
measurement2CachedLastMap.get(measurement), newPair);
+ }
+
+ private int tryUpdateCachedLast(
+ final String measurement, TimeValuePair cachedPair, final TimeValuePair
newPair) {
+ while (Objects.nonNull(cachedPair) && cachedPair.getTimestamp() <=
newPair.getTimestamp()) {
+ if (measurement2CachedLastMap.replace(measurement, cachedPair, newPair))
{
+ return getDiffSize(cachedPair, newPair)
+ + clearKnownNullTimeIfCovered(measurement, newPair.getTimestamp());
}
- measurement2CachedLastMap.computeIfPresent(
- measurement,
- (measurementName, tvPair) -> {
- if (tvPair.getTimestamp() <=
timeValuePairs[finalI].getTimestamp()) {
- diff.addAndGet(
- getDiffSize(tvPair, timeValuePairs[finalI])
- + clearKnownNullTimeIfCovered(
- measurementName,
timeValuePairs[finalI].getTimestamp()));
- return timeValuePairs[finalI];
- }
- return tvPair;
- });
+ cachedPair = measurement2CachedLastMap.get(measurement);
+ }
+ return 0;
+ }
+
+ private void tryUpdateLastTime(final long lastTime) {
+ if (lastTime == Long.MIN_VALUE) {
+ return;
+ }
+ TimeValuePair cachedPair = measurement2CachedLastMap.get("");
+ if (Objects.isNull(cachedPair) || cachedPair.getTimestamp() >= lastTime) {
+ return;
+ }
+ final TimeValuePair newPair = new TimeValuePair(lastTime,
PLACEHOLDER_NO_VALUE);
+ while (Objects.nonNull(cachedPair) && cachedPair.getTimestamp() <
lastTime) {
+ if (measurement2CachedLastMap.replace("", cachedPair, newPair)) {
+ return;
+ }
+ cachedPair = measurement2CachedLastMap.get("");
}
- final long finalLastTime = lastTime;
- measurement2CachedLastMap.computeIfPresent(
- "",
- (time, tvPair) ->
- tvPair.getTimestamp() < finalLastTime
- ? new TimeValuePair(finalLastTime, PLACEHOLDER_NO_VALUE)
- : tvPair);
- return diff.get();
}
@Nullable
@@ -360,8 +405,9 @@ public class TableDeviceLastCache {
return 0;
}
final Long knownNullTime =
measurement2CachedLastKnownNullTimeMap.get(measurement);
- if (knownNullTime != null && knownNullTime <= coveredTime) {
- measurement2CachedLastKnownNullTimeMap.remove(measurement);
+ if (knownNullTime != null
+ && knownNullTime <= coveredTime
+ && measurement2CachedLastKnownNullTimeMap.remove(measurement,
knownNullTime)) {
return -getKnownNullTimeEntrySize();
}
return 0;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceSchemaCache.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceSchemaCache.java
index f712b668689..15bd7ab6eca 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceSchemaCache.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceSchemaCache.java
@@ -305,6 +305,20 @@ public class TableDeviceSchemaCache {
false);
}
+ public void updateLastCacheIfExists(
+ final String database,
+ final IDeviceID deviceId,
+ final String[] measurements,
+ final @Nullable IMeasurementSchema[] measurementSchemas,
+ final LastCacheUpdateSource updateSource) {
+ dualKeyCache.update(
+ new TableId(database, deviceId.getTableName()),
+ deviceId,
+ null,
+ entry -> entry.tryUpdateLastCache(measurements, measurementSchemas,
updateSource),
+ false);
+ }
+
/**
* Update the last cache in writing or the second push of last cache query.
If a measurement is
* with all {@code null}s or is a tag/attribute column, its {@link
TimeValuePair}[] shall be
@@ -457,7 +471,7 @@ public class TableDeviceSchemaCache {
dualKeyCache.update(
new TableId(null, deviceID.getTableName()),
deviceID,
- new TableDeviceCacheEntry(),
+ Objects.isNull(timeValuePairs) ? new TableDeviceCacheEntry() : null,
initOrInvalidate
? entry ->
entry.setMeasurementSchema(
@@ -475,6 +489,26 @@ public class TableDeviceSchemaCache {
Objects.isNull(timeValuePairs));
}
+ void updateLastCache(
+ final String database,
+ final IDeviceID deviceID,
+ final String[] measurements,
+ final LastCacheUpdateSource updateSource,
+ final boolean isAligned,
+ final IMeasurementSchema[] measurementSchemas) {
+ final String previousDatabase =
treeModelDatabasePool.putIfAbsent(database, database);
+ final String database2Use = Objects.nonNull(previousDatabase) ?
previousDatabase : database;
+
+ dualKeyCache.update(
+ new TableId(null, deviceID.getTableName()),
+ deviceID,
+ null,
+ entry ->
+ entry.setMeasurementSchema(database2Use, isAligned, measurements,
measurementSchemas)
+ + entry.tryUpdateLastCache(measurements, measurementSchemas,
updateSource),
+ false);
+ }
+
public boolean getLastCache(
final Map<TableId, Map<IDeviceID, Map<String, Pair<TSDataType,
TimeValuePair>>>> inputMap) {
return dualKeyCache.batchApply(inputMap,
TableDeviceCacheEntry::updateInputMap);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TreeDeviceSchemaCacheManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TreeDeviceSchemaCacheManager.java
index 8cabd8ca43f..799f938edd4 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TreeDeviceSchemaCacheManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TreeDeviceSchemaCacheManager.java
@@ -353,6 +353,17 @@ public class TreeDeviceSchemaCacheManager {
database, deviceID, measurements, timeValuePairs, isAligned,
measurementSchemas, false);
}
+ public void updateLastCacheIfExists(
+ final String database,
+ final IDeviceID deviceID,
+ final String[] measurements,
+ final LastCacheUpdateSource updateSource,
+ final boolean isAligned,
+ final IMeasurementSchema[] measurementSchemas) {
+ tableDeviceSchemaCache.updateLastCache(
+ database, deviceID, measurements, updateSource, isAligned,
measurementSchemas);
+ }
+
/**
* Update the {@link TableDeviceLastCache} on query in tree model.
*
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/cache/TreeDeviceSchemaCacheManagerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/cache/TreeDeviceSchemaCacheManagerTest.java
index f9188aea9af..c11fc1865a7 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/cache/TreeDeviceSchemaCacheManagerTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/cache/TreeDeviceSchemaCacheManagerTest.java
@@ -50,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.apache.iotdb.commons.schema.SchemaConstant.ALL_MATCH_PATTERN;
@@ -275,37 +276,49 @@ public class TreeDeviceSchemaCacheManagerTest {
}
@Test
- public void testUpdateLastCacheWithAliasDoesNotCopyMeasurements() throws
IllegalPathException {
+ public void testUpdateLastCacheLazilyWithAlias() throws IllegalPathException
{
final String database = "root.db";
final PartialPath device = new PartialPath("root.db.d_alias");
final MeasurementSchema s1 = new MeasurementSchema("s1", TSDataType.INT32);
+ final MeasurementSchema s2 = new MeasurementSchema("s2", TSDataType.INT32);
final MeasurementPath s1Path = new
MeasurementPath(device.concatNode("s1"), s1);
+ final AtomicInteger composedValueCount = new AtomicInteger();
treeDeviceSchemaCacheManager.declareLastCache(database, s1Path);
final InsertRowNode insertRowNode =
new InsertRowNode(
- new
PlanNodeId("testUpdateLastCacheWithAliasDoesNotCopyMeasurements"),
+ new PlanNodeId("testUpdateLastCacheLazilyWithAlias"),
device,
false,
- new String[] {"alias"},
- new TSDataType[] {TSDataType.INT32},
- new MeasurementSchema[] {s1},
+ new String[] {"alias", "uncachedAlias"},
+ new TSDataType[] {TSDataType.INT32, TSDataType.INT32},
+ new MeasurementSchema[] {s1, s2},
1L,
- new Object[] {1},
+ new Object[] {1, 2},
false) {
@Override
public String[] getRawMeasurements() {
throw new AssertionError("Last cache update should not copy raw
measurements");
}
+
+ @Override
+ public TimeValuePair composeTimeValuePair(final int columnIndex) {
+ composedValueCount.incrementAndGet();
+ return super.composeTimeValuePair(columnIndex);
+ }
};
insertRowNode.updateLastCache(database);
+ Assert.assertEquals(1, composedValueCount.get());
Assert.assertEquals(
new TimeValuePair(1L, new TsPrimitiveType.TsInt(1)),
treeDeviceSchemaCacheManager.getLastCache(
new MeasurementPath(device.getIDeviceID(), "s1")));
+ Assert.assertNull(
+ treeDeviceSchemaCacheManager.getLastCache(
+ new MeasurementPath(device.getIDeviceID(), "s2")));
Assert.assertNull(
treeDeviceSchemaCacheManager.getLastCache(
new MeasurementPath(device.getIDeviceID(), "alias")));
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimatorPerformanceTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimatorPerformanceTest.java
index d7c01a5b676..655347b37cd 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimatorPerformanceTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/resource/memory/InsertNodeMemoryEstimatorPerformanceTest.java
@@ -22,7 +22,12 @@ package org.apache.iotdb.db.pipe.resource.memory;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Measurement;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Summary;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.write.schema.MeasurementSchema;
@@ -30,6 +35,8 @@ import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
+import java.util.Locale;
+
public class InsertNodeMemoryEstimatorPerformanceTest {
private static final String ENABLED_PROPERTY =
@@ -40,6 +47,18 @@ public class InsertNodeMemoryEstimatorPerformanceTest {
"iotdb.pipe.insert.node.memory.estimator.perf.warmup.iterations";
private static final String ITERATIONS_PROPERTY =
"iotdb.pipe.insert.node.memory.estimator.perf.iterations";
+ private static final String REUSE_ENABLED_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.enabled";
+ private static final String REUSE_ROWS_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.rows";
+ private static final String REUSE_MEASUREMENTS_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.measurements";
+ private static final String REUSE_WARMUP_ITERATIONS_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.warmup.iterations";
+ private static final String REUSE_ITERATIONS_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.iterations";
+ private static final String REUSE_ROUNDS_PROPERTY =
+ "iotdb.pipe.insert.node.memory.estimator.reuse.perf.rounds";
private static volatile long benchmarkBlackhole;
@@ -71,6 +90,98 @@ public class InsertNodeMemoryEstimatorPerformanceTest {
elapsedNanos / (double) iterations / 1_000_000.0);
}
+ @Test
+ public void compositeInsertRowsReusableSetBenchmark() throws
IllegalPathException {
+ Assume.assumeTrue(
+ String.format(
+ "Manual performance UT. Enable with -D%s=true, optionally tune
-D%s, -D%s, -D%s, -D%s and -D%s.",
+ REUSE_ENABLED_PROPERTY,
+ REUSE_ROWS_PROPERTY,
+ REUSE_MEASUREMENTS_PROPERTY,
+ REUSE_WARMUP_ITERATIONS_PROPERTY,
+ REUSE_ITERATIONS_PROPERTY,
+ REUSE_ROUNDS_PROPERTY),
+ Boolean.getBoolean(REUSE_ENABLED_PROPERTY));
+ Assume.assumeTrue(
+ "Current-thread CPU time and allocation metrics are required.",
+ ManualPerformanceTestUtils.enableThreadMetrics());
+
+ final int rowCount = Integer.getInteger(REUSE_ROWS_PROPERTY, 10);
+ final int measurementCount =
Integer.getInteger(REUSE_MEASUREMENTS_PROPERTY, 500);
+ final int warmupIterations =
Integer.getInteger(REUSE_WARMUP_ITERATIONS_PROPERTY, 1_000);
+ final int iterations = Integer.getInteger(REUSE_ITERATIONS_PROPERTY,
20_000);
+ final int rounds = Integer.getInteger(REUSE_ROUNDS_PROPERTY, 5);
+ Assert.assertTrue(rowCount > 1);
+ Assert.assertTrue(measurementCount > 0);
+ Assert.assertTrue(warmupIterations > 0);
+ Assert.assertTrue(iterations > 0);
+ Assert.assertTrue(rounds > 0);
+
+ final InsertRowsNode insertRowsNode =
createCompositeInsertRowsNode(rowCount, measurementCount);
+ InsertNodeMemoryEstimator.clearReusableDeduplicatedObjectsForTest();
+ final long freshSetEstimate =
InsertNodeMemoryEstimator.sizeOf(insertRowsNode);
+ final long reusedSetEstimate =
InsertNodeMemoryEstimator.sizeOf(insertRowsNode);
+ Assert.assertEquals(freshSetEstimate, reusedSetEstimate);
+
+ for (int i = 0; i < warmupIterations; ++i) {
+ if ((i & 1) == 0) {
+ runFreshSetEstimate(insertRowsNode);
+ runEstimate(insertRowsNode);
+ } else {
+ runEstimate(insertRowsNode);
+ runFreshSetEstimate(insertRowsNode);
+ }
+ }
+
+ final Measurement[] freshSetMeasurements = new Measurement[rounds];
+ final Measurement[] reusedSetMeasurements = new Measurement[rounds];
+ for (int i = 0; i < rounds; ++i) {
+ if ((i & 1) == 0) {
+ freshSetMeasurements[i] =
+ ManualPerformanceTestUtils.measure(
+ iterations,
+
InsertNodeMemoryEstimator::clearReusableDeduplicatedObjectsForTest,
+ () -> runEstimate(insertRowsNode));
+ reusedSetMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runEstimate(insertRowsNode));
+ } else {
+ reusedSetMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runEstimate(insertRowsNode));
+ freshSetMeasurements[i] =
+ ManualPerformanceTestUtils.measure(
+ iterations,
+
InsertNodeMemoryEstimator::clearReusableDeduplicatedObjectsForTest,
+ () -> runEstimate(insertRowsNode));
+ }
+ }
+ InsertNodeMemoryEstimator.clearReusableDeduplicatedObjectsForTest();
+
+ final Summary freshSetSummary =
+ ManualPerformanceTestUtils.summarize(freshSetMeasurements, iterations);
+ final Summary reusedSetSummary =
+ ManualPerformanceTestUtils.summarize(reusedSetMeasurements,
iterations);
+ System.out.printf(
+ Locale.ROOT,
+ "InsertRows memory-estimator set-reuse benchmark: rows=%d,
measurements=%d, warmups=%d, iterations/round=%d, rounds=%d%n",
+ rowCount,
+ measurementCount,
+ warmupIterations,
+ iterations,
+ rounds);
+ printSummary("legacy", freshSetSummary);
+ printSummary("optimized", reusedSetSummary);
+ System.out.printf(
+ Locale.ROOT,
+ " change: CPU speedup=%.2fx, allocation reduction=%.1f%%, peak-heap
reduction=%.1f%%%n",
+ ratio(
+ freshSetSummary.getCpuNanosPerOperation(),
reusedSetSummary.getCpuNanosPerOperation()),
+ reduction(
+ freshSetSummary.getAllocatedBytesPerOperation(),
+ reusedSetSummary.getAllocatedBytesPerOperation()),
+ reduction(
+ freshSetSummary.getPeakHeapDeltaBytes(),
reusedSetSummary.getPeakHeapDeltaBytes()));
+ }
+
private static long runBenchmark(final InsertTabletNode insertTabletNode,
final int iterations) {
final long startTime = System.nanoTime();
for (int i = 0; i < iterations; ++i) {
@@ -79,6 +190,70 @@ public class InsertNodeMemoryEstimatorPerformanceTest {
return System.nanoTime() - startTime;
}
+ private static void runFreshSetEstimate(final InsertRowsNode insertRowsNode)
{
+ InsertNodeMemoryEstimator.clearReusableDeduplicatedObjectsForTest();
+ runEstimate(insertRowsNode);
+ }
+
+ private static void runEstimate(final InsertRowsNode insertRowsNode) {
+ benchmarkBlackhole = InsertNodeMemoryEstimator.sizeOf(insertRowsNode);
+ }
+
+ private static InsertRowsNode createCompositeInsertRowsNode(
+ final int rowCount, final int measurementCount) throws
IllegalPathException {
+ final String[] measurements = new String[measurementCount];
+ final TSDataType[] dataTypes = new TSDataType[measurementCount];
+ final MeasurementSchema[] measurementSchemas = new
MeasurementSchema[measurementCount];
+ final Object[] values = new Object[measurementCount];
+ for (int i = 0; i < measurementCount; ++i) {
+ measurements[i] = "s" + i;
+ dataTypes[i] = TSDataType.INT32;
+ measurementSchemas[i] = new MeasurementSchema(measurements[i],
TSDataType.INT32);
+ values[i] = i;
+ }
+
+ final PlanNodeId planNodeId = new PlanNodeId("composite-memory-estimator");
+ final PartialPath devicePath = new PartialPath("root.memory_estimator.d1");
+ final InsertRowsNode insertRowsNode = new InsertRowsNode(planNodeId);
+ for (int i = 0; i < rowCount; ++i) {
+ insertRowsNode.addOneInsertRowNode(
+ new InsertRowNode(
+ planNodeId,
+ devicePath,
+ false,
+ measurements,
+ dataTypes,
+ measurementSchemas,
+ i,
+ values,
+ false),
+ i);
+ }
+ insertRowsNode.setTargetPath(devicePath);
+ insertRowsNode.setMeasurements(measurements);
+ insertRowsNode.setDataTypes(dataTypes);
+ insertRowsNode.setMeasurementSchemas(measurementSchemas);
+ return insertRowsNode;
+ }
+
+ private static void printSummary(final String label, final Summary summary) {
+ System.out.printf(
+ Locale.ROOT,
+ " %-10s CPU=%.3f us/op, allocated=%.1f bytes/op, peak heap delta=%.3f
MiB%n",
+ label,
+ summary.getCpuNanosPerOperation() / 1_000.0,
+ summary.getAllocatedBytesPerOperation(),
+ summary.getPeakHeapDeltaBytes() / 1024.0 / 1024.0);
+ }
+
+ private static double ratio(final double baseline, final double optimized) {
+ return optimized == 0 ? Double.POSITIVE_INFINITY : baseline / optimized;
+ }
+
+ private static double reduction(final double baseline, final double
optimized) {
+ return baseline == 0 ? 0 : (baseline - optimized) * 100.0 / baseline;
+ }
+
private static InsertTabletNode createWideInsertTabletNode(final int
columnCount)
throws IllegalPathException {
final String[] measurements = new String[columnCount];
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCachePerformanceTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCachePerformanceTest.java
new file mode 100644
index 00000000000..d88169999d3
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCachePerformanceTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.queryengine.plan.relational.metadata.fetcher.cache;
+
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Measurement;
+import org.apache.iotdb.db.utils.ManualPerformanceTestUtils.Summary;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.TimeValuePair;
+import org.apache.tsfile.utils.TsPrimitiveType;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.util.Locale;
+
+public class TableDeviceLastCachePerformanceTest {
+
+ private static final String ENABLED_PROPERTY =
"iotdb.last.cache.write.perf.enabled";
+ private static final String MEASUREMENTS_PROPERTY =
"iotdb.last.cache.write.perf.measurements";
+ private static final String CACHED_INTERVAL_PROPERTY =
+ "iotdb.last.cache.write.perf.cached.interval";
+ private static final String WARMUP_ITERATIONS_PROPERTY =
+ "iotdb.last.cache.write.perf.warmup.iterations";
+ private static final String ITERATIONS_PROPERTY =
"iotdb.last.cache.write.perf.iterations";
+ private static final String ROUNDS_PROPERTY =
"iotdb.last.cache.write.perf.rounds";
+
+ private static volatile int benchmarkBlackhole;
+
+ @Test
+ public void sparseInitializedMeasurementsBenchmark() {
+ Assume.assumeTrue(
+ String.format(
+ "Manual performance UT. Enable with -D%s=true, optionally tune
-D%s, -D%s, -D%s, -D%s and -D%s.",
+ ENABLED_PROPERTY,
+ MEASUREMENTS_PROPERTY,
+ CACHED_INTERVAL_PROPERTY,
+ WARMUP_ITERATIONS_PROPERTY,
+ ITERATIONS_PROPERTY,
+ ROUNDS_PROPERTY),
+ Boolean.getBoolean(ENABLED_PROPERTY));
+ Assume.assumeTrue(
+ "Current-thread CPU time and allocation metrics are required.",
+ ManualPerformanceTestUtils.enableThreadMetrics());
+
+ final int measurementCount = Integer.getInteger(MEASUREMENTS_PROPERTY,
1_000);
+ final int cachedInterval = Integer.getInteger(CACHED_INTERVAL_PROPERTY,
10);
+ final int warmupIterations =
Integer.getInteger(WARMUP_ITERATIONS_PROPERTY, 1_000);
+ final int iterations = Integer.getInteger(ITERATIONS_PROPERTY, 20_000);
+ final int rounds = Integer.getInteger(ROUNDS_PROPERTY, 5);
+ Assert.assertTrue(measurementCount > 0);
+ Assert.assertTrue(cachedInterval > 0 && cachedInterval <=
measurementCount);
+ Assert.assertTrue(warmupIterations > 0);
+ Assert.assertTrue(iterations > 0);
+ Assert.assertTrue(rounds > 0);
+
+ final Scenario legacy = createScenario(measurementCount, cachedInterval);
+ final Scenario optimized = createScenario(measurementCount,
cachedInterval);
+ runLegacyUpdate(legacy);
+ runOptimizedUpdate(optimized);
+ assertCacheEquals(legacy, optimized);
+
+ for (int i = 0; i < warmupIterations; ++i) {
+ if ((i & 1) == 0) {
+ runLegacyUpdate(legacy);
+ runOptimizedUpdate(optimized);
+ } else {
+ runOptimizedUpdate(optimized);
+ runLegacyUpdate(legacy);
+ }
+ }
+
+ final Measurement[] legacyMeasurements = new Measurement[rounds];
+ final Measurement[] optimizedMeasurements = new Measurement[rounds];
+ for (int i = 0; i < rounds; ++i) {
+ if ((i & 1) == 0) {
+ legacyMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runLegacyUpdate(legacy));
+ optimizedMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runOptimizedUpdate(optimized));
+ } else {
+ optimizedMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runOptimizedUpdate(optimized));
+ legacyMeasurements[i] =
+ ManualPerformanceTestUtils.measure(iterations, () ->
runLegacyUpdate(legacy));
+ }
+ }
+
+ assertCacheEquals(legacy, optimized);
+ final Summary legacySummary =
+ ManualPerformanceTestUtils.summarize(legacyMeasurements, iterations);
+ final Summary optimizedSummary =
+ ManualPerformanceTestUtils.summarize(optimizedMeasurements,
iterations);
+ printResult(
+ measurementCount,
+ legacy.cachedMeasurementCount,
+ warmupIterations,
+ iterations,
+ rounds,
+ legacySummary,
+ optimizedSummary);
+ }
+
+ private static Scenario createScenario(final int measurementCount, final int
cachedInterval) {
+ final String[] measurements = new String[measurementCount];
+ final MeasurementSchema[] measurementSchemas = new
MeasurementSchema[measurementCount];
+ final int[] values = new int[measurementCount];
+ for (int i = 0; i < measurementCount; ++i) {
+ measurements[i] = "s" + i;
+ measurementSchemas[i] = new MeasurementSchema(measurements[i],
TSDataType.INT32);
+ values[i] = i;
+ }
+
+ final int cachedMeasurementCount = (measurementCount + cachedInterval - 1)
/ cachedInterval;
+ final String[] cachedMeasurements = new String[cachedMeasurementCount];
+ for (int i = 0; i < cachedMeasurementCount; ++i) {
+ cachedMeasurements[i] = measurements[i * cachedInterval];
+ }
+
+ final TableDeviceLastCache cache = new TableDeviceLastCache(false);
+ cache.initOrInvalidate(null, null, cachedMeasurements, false);
+ return new Scenario(
+ cache,
+ measurements,
+ measurementSchemas,
+ new RowUpdateSource(values),
+ cachedMeasurementCount);
+ }
+
+ private static void runLegacyUpdate(final Scenario scenario) {
+ // Keep the eager implementation from origin/master for comparison.
+ final TimeValuePair[] timeValuePairs = new
TimeValuePair[scenario.measurements.length];
+ for (int i = 0; i < scenario.measurements.length; ++i) {
+ timeValuePairs[i] = scenario.updateSource.getLastCacheValue(i);
+ }
+ benchmarkBlackhole =
+ scenario.cache.tryUpdate(
+ scenario.measurements, scenario.measurementSchemas,
timeValuePairs, false);
+ }
+
+ private static void runOptimizedUpdate(final Scenario scenario) {
+ benchmarkBlackhole =
+ scenario.cache.tryUpdate(
+ scenario.measurements, scenario.measurementSchemas,
scenario.updateSource);
+ }
+
+ private static void assertCacheEquals(
+ final Scenario expectedScenario, final Scenario actualScenario) {
+ for (final String measurement : expectedScenario.measurements) {
+ Assert.assertEquals(
+ expectedScenario.cache.getTimeValuePair(measurement),
+ actualScenario.cache.getTimeValuePair(measurement));
+ }
+ }
+
+ private static void printResult(
+ final int measurementCount,
+ final int cachedMeasurementCount,
+ final int warmupIterations,
+ final int iterations,
+ final int rounds,
+ final Summary legacy,
+ final Summary optimized) {
+ System.out.printf(
+ Locale.ROOT,
+ "Last-cache row-update benchmark: measurements=%d, initialized=%d,
warmups=%d, iterations/round=%d, rounds=%d%n",
+ measurementCount,
+ cachedMeasurementCount,
+ warmupIterations,
+ iterations,
+ rounds);
+ printSummary("legacy", legacy);
+ printSummary("optimized", optimized);
+ System.out.printf(
+ Locale.ROOT,
+ " change: CPU speedup=%.2fx, allocation reduction=%.1f%%, peak-heap
reduction=%.1f%%%n",
+ ratio(legacy.getCpuNanosPerOperation(),
optimized.getCpuNanosPerOperation()),
+ reduction(
+ legacy.getAllocatedBytesPerOperation(),
optimized.getAllocatedBytesPerOperation()),
+ reduction(legacy.getPeakHeapDeltaBytes(),
optimized.getPeakHeapDeltaBytes()));
+ }
+
+ private static void printSummary(final String label, final Summary summary) {
+ System.out.printf(
+ Locale.ROOT,
+ " %-9s CPU=%.3f us/op, allocated=%.1f bytes/op, peak heap delta=%.3f
MiB%n",
+ label,
+ summary.getCpuNanosPerOperation() / 1_000.0,
+ summary.getAllocatedBytesPerOperation(),
+ summary.getPeakHeapDeltaBytes() / 1024.0 / 1024.0);
+ }
+
+ private static double ratio(final double baseline, final double optimized) {
+ return optimized == 0 ? Double.POSITIVE_INFINITY : baseline / optimized;
+ }
+
+ private static double reduction(final double baseline, final double
optimized) {
+ return baseline == 0 ? 0 : (baseline - optimized) * 100.0 / baseline;
+ }
+
+ private static final class Scenario {
+
+ private final TableDeviceLastCache cache;
+ private final String[] measurements;
+ private final MeasurementSchema[] measurementSchemas;
+ private final RowUpdateSource updateSource;
+ private final int cachedMeasurementCount;
+
+ private Scenario(
+ final TableDeviceLastCache cache,
+ final String[] measurements,
+ final MeasurementSchema[] measurementSchemas,
+ final RowUpdateSource updateSource,
+ final int cachedMeasurementCount) {
+ this.cache = cache;
+ this.measurements = measurements;
+ this.measurementSchemas = measurementSchemas;
+ this.updateSource = updateSource;
+ this.cachedMeasurementCount = cachedMeasurementCount;
+ }
+ }
+
+ private static final class RowUpdateSource implements LastCacheUpdateSource {
+
+ private final int[] values;
+
+ private RowUpdateSource(final int[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public long getLastCacheTimestamp() {
+ return 1L;
+ }
+
+ @Override
+ public boolean hasLastCacheValue(final int index) {
+ return true;
+ }
+
+ @Override
+ public TimeValuePair getLastCacheValue(final int index) {
+ return new TimeValuePair(1L, new TsPrimitiveType.TsInt(values[index]));
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCacheTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCacheTest.java
index eedbbfbb4ec..4ae3228013e 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCacheTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/cache/TableDeviceLastCacheTest.java
@@ -28,9 +28,43 @@ import org.junit.Test;
import java.util.Collections;
import java.util.Optional;
import java.util.OptionalLong;
+import java.util.concurrent.atomic.AtomicInteger;
public class TableDeviceLastCacheTest {
+ @Test
+ public void testLazyUpdateOnlyComposesCachedMeasurements() {
+ final TableDeviceLastCache cache = new TableDeviceLastCache(false);
+ cache.initOrInvalidate(null, null, new String[] {"s1"}, false);
+ final AtomicInteger composedValueCount = new AtomicInteger();
+
+ cache.tryUpdate(
+ new String[] {"s1", "s2"},
+ null,
+ new LastCacheUpdateSource() {
+ @Override
+ public long getLastCacheTimestamp() {
+ return 1L;
+ }
+
+ @Override
+ public boolean hasLastCacheValue(final int index) {
+ return true;
+ }
+
+ @Override
+ public TimeValuePair getLastCacheValue(final int index) {
+ composedValueCount.incrementAndGet();
+ return new TimeValuePair(1L, new TsPrimitiveType.TsInt(index + 1));
+ }
+ });
+
+ Assert.assertEquals(1, composedValueCount.get());
+ Assert.assertEquals(
+ new TimeValuePair(1L, new TsPrimitiveType.TsInt(1)),
cache.getTimeValuePair("s1"));
+ Assert.assertNull(cache.getTimeValuePair("s2"));
+ }
+
@Test
public void testKnownNullTimePreservesHistoricalValueAndClearsOnNewerValue()
{
final TableDeviceLastCache cache = new TableDeviceLastCache(false);
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/ManualPerformanceTestUtils.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/ManualPerformanceTestUtils.java
new file mode 100644
index 00000000000..6a489f1afe0
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/utils/ManualPerformanceTestUtils.java
@@ -0,0 +1,186 @@
+/*
+ * 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.utils;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryPoolMXBean;
+import java.lang.management.MemoryType;
+import java.lang.management.MemoryUsage;
+import java.lang.management.ThreadMXBean;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class ManualPerformanceTestUtils {
+
+ private static final ThreadMXBean THREAD_MX_BEAN =
ManagementFactory.getThreadMXBean();
+ private static final com.sun.management.ThreadMXBean ALLOCATION_MX_BEAN =
+ THREAD_MX_BEAN instanceof com.sun.management.ThreadMXBean
+ ? (com.sun.management.ThreadMXBean) THREAD_MX_BEAN
+ : null;
+ private static final Runnable NO_OP = () -> {};
+
+ private ManualPerformanceTestUtils() {}
+
+ public static boolean enableThreadMetrics() {
+ if (!THREAD_MX_BEAN.isCurrentThreadCpuTimeSupported()
+ || ALLOCATION_MX_BEAN == null
+ || !ALLOCATION_MX_BEAN.isThreadAllocatedMemorySupported()) {
+ return false;
+ }
+ try {
+ if (!THREAD_MX_BEAN.isThreadCpuTimeEnabled()) {
+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
+ }
+ if (!ALLOCATION_MX_BEAN.isThreadAllocatedMemoryEnabled()) {
+ ALLOCATION_MX_BEAN.setThreadAllocatedMemoryEnabled(true);
+ }
+ final long threadId = Thread.currentThread().getId();
+ return THREAD_MX_BEAN.getCurrentThreadCpuTime() >= 0
+ && ALLOCATION_MX_BEAN.getThreadAllocatedBytes(threadId) >= 0;
+ } catch (final UnsupportedOperationException | SecurityException ignored) {
+ return false;
+ }
+ }
+
+ public static Measurement measure(final int iterations, final Runnable
operation) {
+ return measure(iterations, NO_OP, operation);
+ }
+
+ public static Measurement measure(
+ final int iterations, final Runnable beforeEachIteration, final Runnable
operation) {
+ final List<MemoryPoolMXBean> heapPools = getHeapMemoryPools();
+ System.gc();
+ System.runFinalization();
+ heapPools.forEach(MemoryPoolMXBean::resetPeakUsage);
+ final long baselineHeapBytes = getUsedHeapBytes(heapPools);
+
+ final long threadId = Thread.currentThread().getId();
+ final long allocatedBytesBefore =
ALLOCATION_MX_BEAN.getThreadAllocatedBytes(threadId);
+ long cpuNanos = 0;
+ for (int i = 0; i < iterations; ++i) {
+ beforeEachIteration.run();
+ final long cpuNanosBefore = THREAD_MX_BEAN.getCurrentThreadCpuTime();
+ operation.run();
+ cpuNanos += THREAD_MX_BEAN.getCurrentThreadCpuTime() - cpuNanosBefore;
+ }
+ final long allocatedBytes =
+ ALLOCATION_MX_BEAN.getThreadAllocatedBytes(threadId) -
allocatedBytesBefore;
+ final long peakHeapDeltaBytes = Math.max(0L, getPeakHeapBytes(heapPools) -
baselineHeapBytes);
+ return new Measurement(cpuNanos, allocatedBytes, peakHeapDeltaBytes);
+ }
+
+ public static Summary summarize(final Measurement[] measurements, final int
iterations) {
+ final long[] cpuNanos = new long[measurements.length];
+ final long[] allocatedBytes = new long[measurements.length];
+ final long[] peakHeapDeltaBytes = new long[measurements.length];
+ for (int i = 0; i < measurements.length; ++i) {
+ cpuNanos[i] = measurements[i].cpuNanos;
+ allocatedBytes[i] = measurements[i].allocatedBytes;
+ peakHeapDeltaBytes[i] = measurements[i].peakHeapDeltaBytes;
+ }
+ return new Summary(
+ median(cpuNanos) / iterations,
+ median(allocatedBytes) / iterations,
+ median(peakHeapDeltaBytes));
+ }
+
+ private static List<MemoryPoolMXBean> getHeapMemoryPools() {
+ final List<MemoryPoolMXBean> heapPools = new ArrayList<>();
+ for (final MemoryPoolMXBean memoryPool :
ManagementFactory.getMemoryPoolMXBeans()) {
+ if (memoryPool.getType() == MemoryType.HEAP && memoryPool.isValid()) {
+ heapPools.add(memoryPool);
+ }
+ }
+ return heapPools;
+ }
+
+ private static long getUsedHeapBytes(final List<MemoryPoolMXBean> heapPools)
{
+ long usedHeapBytes = 0;
+ for (final MemoryPoolMXBean heapPool : heapPools) {
+ final MemoryUsage usage = heapPool.getUsage();
+ if (usage != null) {
+ usedHeapBytes += usage.getUsed();
+ }
+ }
+ return usedHeapBytes;
+ }
+
+ private static long getPeakHeapBytes(final List<MemoryPoolMXBean> heapPools)
{
+ long peakHeapBytes = 0;
+ for (final MemoryPoolMXBean heapPool : heapPools) {
+ final MemoryUsage peakUsage = heapPool.getPeakUsage();
+ if (peakUsage != null) {
+ peakHeapBytes += peakUsage.getUsed();
+ }
+ }
+ return peakHeapBytes;
+ }
+
+ private static double median(final long[] values) {
+ Arrays.sort(values);
+ final int middle = values.length / 2;
+ return (values.length & 1) == 1
+ ? values[middle]
+ : values[middle - 1] + (values[middle] - values[middle - 1]) / 2.0;
+ }
+
+ public static final class Measurement {
+
+ private final long cpuNanos;
+ private final long allocatedBytes;
+ private final long peakHeapDeltaBytes;
+
+ private Measurement(
+ final long cpuNanos, final long allocatedBytes, final long
peakHeapDeltaBytes) {
+ this.cpuNanos = cpuNanos;
+ this.allocatedBytes = allocatedBytes;
+ this.peakHeapDeltaBytes = peakHeapDeltaBytes;
+ }
+ }
+
+ public static final class Summary {
+
+ private final double cpuNanosPerOperation;
+ private final double allocatedBytesPerOperation;
+ private final double peakHeapDeltaBytes;
+
+ private Summary(
+ final double cpuNanosPerOperation,
+ final double allocatedBytesPerOperation,
+ final double peakHeapDeltaBytes) {
+ this.cpuNanosPerOperation = cpuNanosPerOperation;
+ this.allocatedBytesPerOperation = allocatedBytesPerOperation;
+ this.peakHeapDeltaBytes = peakHeapDeltaBytes;
+ }
+
+ public double getCpuNanosPerOperation() {
+ return cpuNanosPerOperation;
+ }
+
+ public double getAllocatedBytesPerOperation() {
+ return allocatedBytesPerOperation;
+ }
+
+ public double getPeakHeapDeltaBytes() {
+ return peakHeapDeltaBytes;
+ }
+ }
+}