This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new d9bead0d1e [#7472] feat(core): Add JMH Tests for EntityCache (#7501)
d9bead0d1e is described below
commit d9bead0d1e35822f2eca9d35b6c2a55521ecf026
Author: Lord of Abyss <[email protected]>
AuthorDate: Mon Jul 7 13:59:08 2025 +0800
[#7472] feat(core): Add JMH Tests for EntityCache (#7501)
### What changes were proposed in this pull request?
The following JMH benchmarks were executed under the environment below:
- CPU: Apple M2 Pro
- Memory: 16 GB
- OS: macOS Ventura 15.5
- JVM: openjdk version "17.0.15"
- JMH Version: 1.37
- Benchmark Config:
- `@BenchmarkMode`: Throughput & AverageTime
- `@State`: Scope.Thread
- `@Fork`: 1
- `@Warmup:` 5 iterations
- `@Measurement`: 10 iterations
Add JMH Tests for EntityCache, the result as follows:
```text
Benchmark (totalCnt)
Mode Cnt Score Error Units
EntityCacheClearBenchmark.benchmarkClear 10
thrpt 10 536704.823 ± 36351.106 ops/s
EntityCacheClearBenchmark.benchmarkClear 100
thrpt 10 89391.338 ± 4692.738 ops/s
EntityCacheClearBenchmark.benchmarkClear 1000
thrpt 10 8406.573 ± 738.173 ops/s
EntityCacheContainsBenchmark.benchmarkContains 10
thrpt 10 19008727.286 ± 1172892.696 ops/s
EntityCacheContainsBenchmark.benchmarkContains 100
thrpt 10 19129605.626 ± 928728.148 ops/s
EntityCacheContainsBenchmark.benchmarkContains 1000
thrpt 10 17954917.808 ± 916939.288 ops/s
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 10
thrpt 10 9454829.072 ± 403482.020 ops/s
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 100
thrpt 10 5910924.166 ± 234228.471 ops/s
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 1000
thrpt 10 1021172.939 ± 188152.980 ops/s
EntityCacheGetBenchmark.benchmarkGet 10
thrpt 10 17646662.661 ± 1850512.796 ops/s
EntityCacheGetBenchmark.benchmarkGet 100
thrpt 10 17906401.139 ± 905521.957 ops/s
EntityCacheGetBenchmark.benchmarkGet 1000
thrpt 10 17882451.612 ± 1013749.411 ops/s
EntityCacheGetBenchmark.benchmarkGetWithRelations 10
thrpt 10 7949607.041 ± 323537.818 ops/s
EntityCacheGetBenchmark.benchmarkGetWithRelations 100
thrpt 10 4939219.694 ± 54283.320 ops/s
EntityCacheGetBenchmark.benchmarkGetWithRelations 1000
thrpt 10 1060788.506 ± 22922.363 ops/s
EntityCacheInvalidateBenchmark.benchmarkInvalidate 10
thrpt 10 184227.751 ± 6303.043 ops/s
EntityCacheInvalidateBenchmark.benchmarkInvalidate 100
thrpt 10 19663.536 ± 684.142 ops/s
EntityCacheInvalidateBenchmark.benchmarkInvalidate 1000
thrpt 10 1651.429 ± 213.587 ops/s
EntityCachePutBenchmark.benchmarkPut 10
thrpt 10 222207.294 ± 10992.713 ops/s
EntityCachePutBenchmark.benchmarkPut 100
thrpt 10 20128.455 ± 434.551 ops/s
EntityCachePutBenchmark.benchmarkPut 1000
thrpt 10 1902.510 ± 75.715 ops/s
EntityCachePutBenchmark.benchmarkPutWithRelation 10
thrpt 10 335683.335 ± 34641.231 ops/s
EntityCachePutBenchmark.benchmarkPutWithRelation 100
thrpt 10 26415.590 ± 1511.666 ops/s
EntityCachePutBenchmark.benchmarkPutWithRelation 1000
thrpt 10 2345.904 ± 89.693 ops/s
EntityCacheSizeBenchmark.entityCacheSize 10
thrpt 10 9416189.995 ± 1018247.698 ops/s
EntityCacheSizeBenchmark.entityCacheSize 100
thrpt 10 944383.407 ± 35235.930 ops/s
EntityCacheSizeBenchmark.entityCacheSize 1000
thrpt 10 79938.016 ± 2466.431 ops/s
EntityCacheClearBenchmark.benchmarkClear 10
avgt 10 ≈ 10⁻⁵ s/op
EntityCacheClearBenchmark.benchmarkClear 100
avgt 10 ≈ 10⁻⁴ s/op
EntityCacheClearBenchmark.benchmarkClear 1000
avgt 10 ≈ 10⁻³ s/op
EntityCacheContainsBenchmark.benchmarkContains 10
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheContainsBenchmark.benchmarkContains 100
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheContainsBenchmark.benchmarkContains 1000
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 10
avgt 10 ≈ 10⁻⁶ s/op
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 100
avgt 10 ≈ 10⁻⁶ s/op
EntityCacheContainsBenchmark.benchmarkContainsWithRelation 1000
avgt 10 ≈ 10⁻⁵ s/op
EntityCacheGetBenchmark.benchmarkGet 10
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheGetBenchmark.benchmarkGet 100
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheGetBenchmark.benchmarkGet 1000
avgt 10 ≈ 10⁻⁷ s/op
EntityCacheGetBenchmark.benchmarkGetWithRelations 10
avgt 10 ≈ 10⁻⁶ s/op
EntityCacheGetBenchmark.benchmarkGetWithRelations 100
avgt 10 ≈ 10⁻⁶ s/op
EntityCacheGetBenchmark.benchmarkGetWithRelations 1000
avgt 10 ≈ 10⁻⁵ s/op
EntityCacheInvalidateBenchmark.benchmarkInvalidate 10
avgt 10 ≈ 10⁻⁵ s/op
EntityCacheInvalidateBenchmark.benchmarkInvalidate 100
avgt 10 ≈ 10⁻⁴ s/op
EntityCacheInvalidateBenchmark.benchmarkInvalidate 1000
avgt 10 0.002 ± 0.001 s/op
EntityCachePutBenchmark.benchmarkPut 10
avgt 10 ≈ 10⁻⁵ s/op
EntityCachePutBenchmark.benchmarkPut 100
avgt 10 ≈ 10⁻⁴ s/op
EntityCachePutBenchmark.benchmarkPut 1000
avgt 10 0.002 ± 0.001 s/op
EntityCachePutBenchmark.benchmarkPutWithRelation 10
avgt 10 ≈ 10⁻⁵ s/op
EntityCachePutBenchmark.benchmarkPutWithRelation 100
avgt 10 ≈ 10⁻⁴ s/op
EntityCachePutBenchmark.benchmarkPutWithRelation 1000
avgt 10 0.002 ± 0.001 s/op
EntityCacheSizeBenchmark.entityCacheSize 10
avgt 10 ≈ 10⁻⁶ s/op
EntityCacheSizeBenchmark.entityCacheSize 100
avgt 10 ≈ 10⁻⁵ s/op
EntityCacheSizeBenchmark.entityCacheSize 1000
avgt 10 ≈ 10⁻⁴ s/op
```
The summary of operation complexity as follows
| Operation Type | Time Complexity | Notes |
| -------------- | ------------------------------------ |
------------------------------------------------------------ |
| `contains` | $O(1)$ | Constant-time key lookup |
| `get` | $O(1)$ | Direct key-based retrieval |
| `getWithRel` | $O(K)$ | Lookup is $O(1)$, but list conversion incurs
linear cost over $K$ |
| `put` | $O(1)$ per entry → $O(N)$ overall | No batch insert; iterates
over $N$ entries |
| `putWithRel` | $O(1)$ per relation → $O(N)$ overall | Each related
list is written individually |
| `invalidate` | $O(N)$ | Performed per entry or group of keys |
| `clear` | $O(N)$ | Linear sweep over all cache entries |
| `size()` | $O(N)$ | Scans all entries to compute accurate size |
Although `getWithRel()` performs a key-based lookup in the cache (which
is O(1)), the overall cost is dominated by the conversion step that
transforms a cached `List<Entity>` into a typed `List<E>`.
This conversion involves:
- Iterating over all elements in the cached list;
- Validating and casting each Entity to the expected generic type E.
As a result, the time complexity of the operation becomes $O(K)$, where
K is the size of the cached list. This cost grows linearly with the
number of related entities stored under a single cache key.
### Why are the changes needed?
Fix: #7472
### Does this PR introduce _any_ user-facing change?
no
### How was this patch tested?
local test, `./gradlew :core:jmh`
---
LICENSE.bin | 1 +
build.gradle.kts | 2 +-
core/build.gradle.kts | 16 ++++
.../gravitino/cache/AbstractEntityBenchmark.java | 92 ++++++++++++++++++
.../apache/gravitino/cache/BenchmarkHelper.java | 105 +++++++++++++++++++++
.../gravitino/cache/ClearEntityCacheBenchmark.java | 58 ++++++++++++
.../cache/ContainsEntityCacheBenchmark.java | 66 +++++++++++++
.../gravitino/cache/GetEntityCacheBenchmark.java | 75 +++++++++++++++
.../cache/InvalidateEntityCacheBenchmark.java | 59 ++++++++++++
.../cache/MeasureSizeEntityCacheBenchmark.java | 76 +++++++++++++++
.../gravitino/cache/PutEntityCacheBenchmark.java | 77 +++++++++++++++
.../java/org/apache/gravitino/utils/TestUtil.java | 17 +++-
gradle/libs.versions.toml | 3 +
13 files changed, 644 insertions(+), 3 deletions(-)
diff --git a/LICENSE.bin b/LICENSE.bin
index 4e075212bc..1120dae1c2 100644
--- a/LICENSE.bin
+++ b/LICENSE.bin
@@ -378,6 +378,7 @@
Awaitility
npgall concurrent-trees
reyerizo jcstress-gradle-plugin
+ melix jmh-gradle-plugin
This product bundles various third-party components also under the
Apache Software Foundation License 1.1
diff --git a/build.gradle.kts b/build.gradle.kts
index c929de3506..4adda3e60c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -853,7 +853,7 @@ tasks {
dependsOn("${it.name}:build")
from("${it.name}/build/libs") {
include("*.jar")
- exclude("*-jcstress.jar")
+ exclude("*-jcstress.jar", "*-jmh.jar")
}
into("distribution/package/libs")
setDuplicatesStrategy(DuplicatesStrategy.INCLUDE)
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 43e4cb5574..65129aab15 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -23,6 +23,7 @@ plugins {
id("java")
id("idea")
alias(libs.plugins.jcstress)
+ alias(libs.plugins.jmh)
}
dependencies {
@@ -82,6 +83,11 @@ tasks.withType<JavaCompile>().configureEach {
}
}
+tasks.named<JavaCompile>("jmhCompileGeneratedClasses").configure {
+ options.errorprone?.isEnabled = false
+ options.compilerArgs.removeAll { it.contains("Xplugin:ErrorProne") }
+}
+
jcstress {
/*
Available modes:
@@ -93,3 +99,13 @@ jcstress {
mode = "default"
jvmArgsPrepend = "-Djdk.stdout.sync=true"
}
+
+jmh {
+ jmhVersion.set(libs.versions.jmh.asProvider())
+ warmupIterations = 5
+ iterations = 10
+ fork = 1
+ threads = 4
+ resultFormat = "csv"
+ resultsFile = file("$buildDir/reports/jmh/results.csv")
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/AbstractEntityBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/AbstractEntityBenchmark.java
new file mode 100644
index 0000000000..a90ce8208c
--- /dev/null
+++ b/core/src/jmh/java/org/apache/gravitino/cache/AbstractEntityBenchmark.java
@@ -0,0 +1,92 @@
+/*
+ * 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.gravitino.cache;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+/**
+ * AbstractEntityBenchmark is a JMH-based benchmark base class designed to
evaluate the performance
+ * of the {@link EntityCache} implementation in Gravitino.
+ *
+ * <p>This class sets up a controlled environment where a configurable number
of entities and their
+ * associated relations are preloaded into the cache before each iteration. It
supports benchmarking
+ * cache operations such as put, get, and relation-based access under varying
data volumes.
+ *
+ * <p>Features:
+ *
+ * <ul>
+ * <li>Supports benchmarking with different entity counts (e.g., 10, 100,
1000)
+ * <li>Measures both throughput and average execution time using JMH modes
+ * <li>Preloads both simple entities and relation entities (e.g., user-role
mappings)
+ * </ul>
+ *
+ * <p>Subclasses should implement benchmark methods to evaluate specific cache
operations.
+ *
+ * @param <E> the type of entity under test, must implement {@link Entity} and
{@link HasIdentifier}
+ * @see org.apache.gravitino.cache.CaffeineEntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ * @see org.apache.gravitino.cache.BenchmarkHelper
+ */
+@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
+@OutputTimeUnit(TimeUnit.SECONDS)
+@State(Scope.Thread)
+public abstract class AbstractEntityBenchmark<E extends Entity &
HasIdentifier> {
+ @Param({"10", "100", "1000"})
+ public int totalCnt;
+
+ protected EntityCache cache;
+ protected List<ModelEntity> entities;
+ protected Map<RoleEntity, List<E>> entitiesWithRelations;
+ protected Random random = new Random();
+
+ @Setup(Level.Iteration)
+ public void setup() {
+ Config config = new Config() {};
+ this.cache = new CaffeineEntityCache(config);
+ this.entities = BenchmarkHelper.getEntities(totalCnt);
+ this.entitiesWithRelations = BenchmarkHelper.getRelationEntities(totalCnt);
+
+ entities.forEach(cache::put);
+ entitiesWithRelations.forEach(
+ (roleEntity, userList) ->
+ cache.put(
+ roleEntity.nameIdentifier(),
+ roleEntity.type(),
+ SupportsRelationOperations.Type.ROLE_USER_REL,
+ userList));
+ }
+}
diff --git a/core/src/jmh/java/org/apache/gravitino/cache/BenchmarkHelper.java
b/core/src/jmh/java/org/apache/gravitino/cache/BenchmarkHelper.java
new file mode 100644
index 0000000000..b8e3f1815c
--- /dev/null
+++ b/core/src/jmh/java/org/apache/gravitino/cache/BenchmarkHelper.java
@@ -0,0 +1,105 @@
+/*
+ * 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.gravitino.cache;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.utils.TestUtil;
+
+public class BenchmarkHelper {
+ public static final int RELATION_CNT = 5;
+ private static final Random random = new Random();
+
+ private BenchmarkHelper() {
+ // Utility class, no constructor needed
+ }
+
+ /**
+ * Generates a map of entities with their relations.
+ *
+ * @param entityCnt the count of entities to generate.
+ * @return a map of entities with their relations.
+ * @param <E> the type of the entity.
+ */
+ public static <E extends Entity & HasIdentifier> Map<RoleEntity, List<E>>
getRelationEntities(
+ int entityCnt) {
+ Map<RoleEntity, List<E>> entitiesWithRelations = Maps.newHashMap();
+ for (int i = 0; i < entityCnt; i++) {
+ RoleEntity testRoleEntity = TestUtil.getTestRoleEntity();
+ int userCnt = random.nextInt(RELATION_CNT);
+ List<E> userList =
BaseEntityCache.convertEntities(getUserList(testRoleEntity, userCnt));
+ entitiesWithRelations.put(testRoleEntity, userList);
+ }
+
+ return entitiesWithRelations;
+ }
+
+ /**
+ * Generates a list of entities with the specified count.
+ *
+ * @param entityCnt the count of entities to generate.
+ * @return a list of model entities.
+ */
+ public static List<ModelEntity> getEntities(int entityCnt) {
+ List<ModelEntity> entities = new ArrayList<>(entityCnt);
+ for (int i = 0; i < entityCnt; i++) {
+ entities.add(TestUtil.getTestModelEntity());
+ }
+
+ return entities;
+ }
+
+ /**
+ * Returns a randomly selected key from the given map.
+ *
+ * @param map the input map from which to select a random key
+ * @param <K> the type of keys in the map
+ * @param <V> the type of values in the map
+ * @return a randomly selected key from the map, or {@code null} if the map
is null or empty
+ */
+ public static <K, V> K getRandomKey(Map<K, V> map) {
+ if (map == null || map.isEmpty()) {
+ return null;
+ }
+
+ List<K> keys = new ArrayList<>(map.keySet());
+
+ return keys.get(random.nextInt(keys.size()));
+ }
+
+ private static List<Entity> getUserList(RoleEntity roleEntity, int userCnt) {
+ List<Entity> userList = new ArrayList<>(userCnt);
+ List<Long> roleIds = ImmutableList.of(roleEntity.id());
+
+ for (int i = 0; i < userCnt; i++) {
+ userList.add(TestUtil.getTestUserEntity(roleIds));
+ }
+
+ return userList;
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/ClearEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/ClearEntityCacheBenchmark.java
new file mode 100644
index 0000000000..4ccaea4079
--- /dev/null
+++
b/core/src/jmh/java/org/apache/gravitino/cache/ClearEntityCacheBenchmark.java
@@ -0,0 +1,58 @@
+/*
+ * 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.gravitino.cache;
+
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.meta.ModelEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Setup;
+
+/**
+ * EntityCacheClearBenchmark benchmarks the performance of the {@link
EntityCache#clear()} operation
+ * under varying cache sizes.
+ *
+ * <p>This benchmark repeatedly clears the cache to evaluate the efficiency
and overhead of the
+ * cache invalidation mechanism, especially under different entity volumes
(e.g., 10, 100, 1000).
+ *
+ * <p>Before each invocation, the cache is pre-populated with a predefined
list of entities to
+ * ensure the {@code clear()} method operates on a non-empty cache.
+ *
+ * @param <E> the entity type being tested, must extend {@link Entity} and
implement {@link
+ * HasIdentifier}
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class ClearEntityCacheBenchmark<E extends Entity & HasIdentifier>
+ extends AbstractEntityBenchmark {
+
+ @Setup(Level.Invocation)
+ @SuppressWarnings("unchecked")
+ public void prepareCacheForClear() {
+ cache.clear();
+ entities.forEach(e -> cache.put((ModelEntity) e));
+ }
+
+ @Benchmark
+ public void benchmarkClear() {
+ cache.clear();
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/ContainsEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/ContainsEntityCacheBenchmark.java
new file mode 100644
index 0000000000..905500d1af
--- /dev/null
+++
b/core/src/jmh/java/org/apache/gravitino/cache/ContainsEntityCacheBenchmark.java
@@ -0,0 +1,66 @@
+/*
+ * 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.gravitino.cache;
+
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+
+/**
+ * EntityCacheContainsBenchmark benchmarks the performance of the {@link
EntityCache#contains}
+ * operation for both regular entities and relation-based entries.
+ *
+ * <p>This benchmark evaluates how efficiently the cache can determine whether
a given entity or a
+ * relationship entry exists, under different data volumes (e.g., 10, 100,
1000).
+ *
+ * <p>It includes two benchmark methods:
+ *
+ * <ul>
+ * <li>{@code benchmarkContains}: Checks the presence of a single {@link
ModelEntity} in the
+ * cache.
+ * <li>{@code benchmarkContainsWithRelation}: Checks whether a relation
entry (e.g., role-to-user
+ * mapping) exists for a given {@link RoleEntity} and relation type.
+ * </ul>
+ *
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class ContainsEntityCacheBenchmark extends AbstractEntityBenchmark {
+
+ @Benchmark
+ public boolean benchmarkContains() {
+ int idx = random.nextInt(entities.size());
+ ModelEntity sampleEntity = (ModelEntity) entities.get(idx);
+
+ return cache.contains(sampleEntity.nameIdentifier(), sampleEntity.type());
+ }
+
+ @Benchmark
+ @SuppressWarnings("unchecked")
+ public boolean benchmarkContainsWithRelation() {
+ RoleEntity sampleRoleEntity = (RoleEntity)
BenchmarkHelper.getRandomKey(entitiesWithRelations);
+
+ return cache.contains(
+ sampleRoleEntity.nameIdentifier(),
+ sampleRoleEntity.type(),
+ SupportsRelationOperations.Type.ROLE_USER_REL);
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/GetEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/GetEntityCacheBenchmark.java
new file mode 100644
index 0000000000..b19a5e1ae2
--- /dev/null
+++ b/core/src/jmh/java/org/apache/gravitino/cache/GetEntityCacheBenchmark.java
@@ -0,0 +1,75 @@
+/*
+ * 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.gravitino.cache;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+
+/**
+ * EntityCacheGetBenchmark benchmarks the performance of the {@link
EntityCache#getIfPresent}
+ * operation for both individual entities and entity relations.
+ *
+ * <p>This benchmark measures the efficiency of cache lookups under varying
data sizes (e.g., 10,
+ * 100, 1000 entries), helping evaluate the speed and consistency of the
cache's get operations.
+ *
+ * <p>It includes two benchmark methods:
+ *
+ * <ul>
+ * <li>{@code benchmarkGet}: Retrieves a {@link ModelEntity} by its
identifier and type.
+ * <li>{@code benchmarkGetWithRelations}: Retrieves a list of related
entities (e.g., users under
+ * a role) using a relation type and a {@link RoleEntity} as the lookup
key.
+ * </ul>
+ *
+ * @param <E> the type of related entity, extending {@link Entity} and
implementing {@link
+ * HasIdentifier}
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class GetEntityCacheBenchmark<E extends Entity & HasIdentifier>
+ extends AbstractEntityBenchmark {
+
+ @Benchmark
+ public Entity benchmarkGet() {
+ int idx = random.nextInt(entities.size());
+ ModelEntity sampleEntity = (ModelEntity) entities.get(idx);
+
+ return cache.getIfPresent(sampleEntity.nameIdentifier(),
sampleEntity.type()).orElse(null);
+ }
+
+ @Benchmark
+ @SuppressWarnings("unchecked")
+ public List<E> benchmarkGetWithRelations() {
+ RoleEntity sampleRoleEntity = (RoleEntity)
BenchmarkHelper.getRandomKey(entitiesWithRelations);
+
+ Optional<List<E>> relationFromCache =
+ cache.getIfPresent(
+ SupportsRelationOperations.Type.ROLE_USER_REL,
+ sampleRoleEntity.nameIdentifier(),
+ sampleRoleEntity.type());
+
+ return relationFromCache.orElse(null);
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/InvalidateEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/InvalidateEntityCacheBenchmark.java
new file mode 100644
index 0000000000..9eb3f202a3
--- /dev/null
+++
b/core/src/jmh/java/org/apache/gravitino/cache/InvalidateEntityCacheBenchmark.java
@@ -0,0 +1,59 @@
+/*
+ * 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.gravitino.cache;
+
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.meta.ModelEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Setup;
+
+/**
+ * EntityCacheInvalidateBenchmark benchmarks the performance of the {@link
EntityCache#invalidate}
+ * operation for a single entity entry.
+ *
+ * <p>Before each benchmark invocation, the cache is repopulated with a
predefined list of entities
+ * to ensure that the invalidation targets a present and consistent entry.
+ *
+ * <p>The benchmark focuses on the following scenario:
+ *
+ * <ul>
+ * <li>{@code benchmarkInvalidate}: Invalidates a single {@link Entity}
identified by a fixed
+ * {@link NameIdentifier} and {@link Entity.EntityType}.
+ * </ul>
+ *
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.apache.gravitino.cache.CaffeineEntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class InvalidateEntityCacheBenchmark extends AbstractEntityBenchmark {
+
+ @Setup(Level.Invocation)
+ @SuppressWarnings("unchecked")
+ public void prepareCacheForClear() {
+ entities.forEach(e -> cache.put((ModelEntity) e));
+ }
+
+ @Benchmark
+ public void benchmarkInvalidate() {
+ cache.invalidate(NameIdentifier.of("m1"), Entity.EntityType.METALAKE);
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/MeasureSizeEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/MeasureSizeEntityCacheBenchmark.java
new file mode 100644
index 0000000000..5643feb10e
--- /dev/null
+++
b/core/src/jmh/java/org/apache/gravitino/cache/MeasureSizeEntityCacheBenchmark.java
@@ -0,0 +1,76 @@
+/*
+ * 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.gravitino.cache;
+
+import java.util.List;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+
+/**
+ * EntityCacheSizeBenchmark benchmarks the performance and overhead of
querying the cache size via
+ * {@link EntityCache#size()} under varying data volumes.
+ *
+ * <p>During setup, both standard entities and relation-based entities are
preloaded into the cache
+ * using the configured entity count. This ensures that the {@code size()}
method operates on a
+ * fully populated cache with realistic structure and distribution.
+ *
+ * <p>The benchmark includes a single method:
+ *
+ * <ul>
+ * <li>{@code entityCacheSize}: Measures the execution time of retrieving
the total number of
+ * cached entries.
+ * </ul>
+ *
+ * @param <E> the type of related entity, extending {@link Entity} and
implementing {@link
+ * HasIdentifier}
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class MeasureSizeEntityCacheBenchmark<E extends Entity & HasIdentifier>
+ extends AbstractEntityBenchmark {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setup() {
+ Config config = new Config() {};
+ this.cache = new CaffeineEntityCache(config);
+ this.entities = BenchmarkHelper.getEntities(totalCnt);
+ this.entitiesWithRelations = BenchmarkHelper.getRelationEntities(totalCnt);
+
+ entities.forEach(e -> cache.put((ModelEntity) e));
+ entitiesWithRelations.forEach(
+ (roleEntity, userList) ->
+ cache.put(
+ ((RoleEntity) roleEntity).nameIdentifier(),
+ ((RoleEntity) roleEntity).type(),
+ SupportsRelationOperations.Type.ROLE_USER_REL,
+ (List<E>) userList));
+ }
+
+ @Benchmark
+ public long entityCacheSize() {
+ return cache.size();
+ }
+}
diff --git
a/core/src/jmh/java/org/apache/gravitino/cache/PutEntityCacheBenchmark.java
b/core/src/jmh/java/org/apache/gravitino/cache/PutEntityCacheBenchmark.java
new file mode 100644
index 0000000000..1a20a08549
--- /dev/null
+++ b/core/src/jmh/java/org/apache/gravitino/cache/PutEntityCacheBenchmark.java
@@ -0,0 +1,77 @@
+/*
+ * 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.gravitino.cache;
+
+import java.util.List;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Setup;
+
+/**
+ * EntityCachePutBenchmark benchmarks the performance of {@link
EntityCache#put} operations for both
+ * standard entities and relation-based entity groups.
+ *
+ * <p>Each benchmark invocation starts with a cleared cache to ensure that
measurements reflect
+ * fresh insertions without side effects from previous state.
+ *
+ * <p>This benchmark covers two insertion scenarios:
+ *
+ * <ul>
+ * <li>{@code benchmarkPut}: Inserts a batch of {@link ModelEntity}
instances into the cache.
+ * <li>{@code benchmarkPutWithRelation}: Inserts relation mappings between
{@link RoleEntity} and
+ * related entities (e.g., users), using a relation type as part of the
cache key.
+ * </ul>
+ *
+ * @param <E> the type of related entity, extending {@link Entity} and
implementing {@link
+ * HasIdentifier}
+ * @see org.apache.gravitino.cache.EntityCache
+ * @see org.openjdk.jmh.annotations.Benchmark
+ */
+public class PutEntityCacheBenchmark<E extends Entity & HasIdentifier>
+ extends AbstractEntityBenchmark {
+
+ @Setup(Level.Invocation)
+ public void prepareForCachePut() {
+ cache.clear();
+ }
+
+ @Benchmark
+ @SuppressWarnings("unchecked")
+ public void benchmarkPut() {
+ entities.forEach(e -> cache.put((ModelEntity) e));
+ }
+
+ @Benchmark
+ @SuppressWarnings("unchecked")
+ public void benchmarkPutWithRelation() {
+ entitiesWithRelations.forEach(
+ (role, relationEntities) ->
+ cache.put(
+ ((RoleEntity) role).nameIdentifier(),
+ ((RoleEntity) role).type(),
+ SupportsRelationOperations.Type.ROLE_USER_REL,
+ (List<E>) relationEntities));
+ }
+}
diff --git a/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
b/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
index 1242a342ed..fc909dc6a1 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
@@ -171,7 +171,8 @@ public class TestUtil {
* @return The test {@link TableEntity} entity.
*/
public static ModelEntity getTestModelEntity() {
- return getTestModelEntity(generator.nextId(), "test_model",
Namespace.of("m1", "c1", "s1"));
+ return getTestModelEntity(
+ generator.nextId(), RandomNameUtils.genRandomName("model"),
Namespace.of("m1", "c1", "s1"));
}
/**
@@ -365,6 +366,17 @@ public class TestUtil {
generator.nextId(), "test_user", "test_metalake",
ImmutableList.of(1L));
}
+ /**
+ * Returns a test user entity with the given ID, name, and roles.
+ *
+ * @param roles The roles of the user entity.
+ * @return The test {@link UserEntity} entity.
+ */
+ public static UserEntity getTestUserEntity(List<Long> roles) {
+ return getTestUserEntity(
+ generator.nextId(), RandomNameUtils.genRandomName("user"),
"test_metalake", roles);
+ }
+
/**
* Returns a test user entity with the given ID, name, and metalake.
*
@@ -421,7 +433,8 @@ public class TestUtil {
* @return The test {@link RoleEntity} entity.
*/
public static RoleEntity getTestRoleEntity() {
- return getTestRoleEntity(generator.nextId(), "test_role", "test_metalake");
+ return getTestRoleEntity(
+ generator.nextId(), RandomNameUtils.genRandomName("role"),
"test_metalake");
}
/**
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0796f97ca8..8330ca8759 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -121,6 +121,8 @@ aliyun-credentials = "0.3.12"
openlineage = "1.29.0"
concurrent-trees = "2.6.0"
jcstress = "0.8.15"
+jmh-plugin = "0.7.3"
+jmh = "1.37"
[libraries]
aws-iam = { group = "software.amazon.awssdk", name = "iam", version.ref =
"awssdk" }
@@ -309,3 +311,4 @@ dependencyLicenseReport = {id =
"com.github.jk1.dependency-license-report", vers
bom = {id = "org.cyclonedx.bom", version = "1.5.0"}
errorprone = {id = "net.ltgt.errorprone", version.ref = "error-prone"}
jcstress = { id = "io.github.reyerizo.gradle.jcstress", version.ref =
"jcstress" }
+jmh = { id = "me.champeau.jmh", version.ref = "jmh-plugin" }
\ No newline at end of file