Skip to content

Commit c3174fc

Browse files
committed
- 性能:优化资源包 resourceExists(Classpath/Directory 索引 + 路径规范化)。
- 性能:OptiFine 场景提前 prewarm minecraft 索引,减少 Class#getResource 回退。 - 性能:DefaultResourcePack 未启用缓存时也优先走索引。 - 性能:StateMapperBase 并行模式使用 NonBlockingIdentityHashMap(IBlockState identity)。 - 性能:ResourceLoader/FolderResourcePack 跳过 canonical 校验并预热目录索引。 - 开发:启用 JMH 并新增 ParallelModelLoader map benchmark。
1 parent b1b1bd1 commit c3174fc

22 files changed

Lines changed: 1581 additions & 31 deletions

build.gradle.kts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ plugins {
88
id("org.jetbrains.gradle.plugin.idea-ext") version "1.1.7"
99
id("eclipse")
1010
id("com.gtnewhorizons.retrofuturagradle") version "1.3.34"
11+
id("me.champeau.jmh") version "0.7.2"
1112
}
1213

1314
// Project properties
1415
group = "github.kasuminova.stellarcore"
15-
version = "1.5.23"
16+
version = "1.6.0"
1617

1718
// Set the toolchain version to decouple the Java we run Gradle with from the Java used to compile and run the mod
1819
java {
@@ -90,6 +91,18 @@ tasks.compileTestJava.configure {
9091
}
9192
}
9293

94+
tasks.withType<JavaCompile>().configureEach {
95+
// Ensure JMH compilation uses the same target/runtime constraints.
96+
// (compileJmhJava is contributed by me.champeau.jmh)
97+
sourceCompatibility = "17"
98+
options.release = 8
99+
options.encoding = "UTF-8"
100+
101+
javaCompiler = javaToolchains.compilerFor {
102+
languageVersion = JavaLanguageVersion.of(17)
103+
}
104+
}
105+
93106
tasks.javadoc.configure {
94107
// No need for JavaDoc.
95108
actions = Collections.emptyList()
@@ -128,9 +141,6 @@ listOf(configurations.runtimeClasspath, configurations.testRuntimeClasspath).for
128141

129142
// Dependencies
130143
repositories {
131-
flatDir {
132-
dirs("lib")
133-
}
134144
maven {
135145
url = uri("https://maven.aliyun.com/repository/public")
136146
}
@@ -161,6 +171,10 @@ repositories {
161171
name = "GeckoLib"
162172
url = uri("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/")
163173
}
174+
maven {
175+
// CraftTweaker
176+
url = uri("https://maven.blamejared.com/")
177+
}
164178
maven {
165179
name = "OvermindDL1 Maven"
166180
url = uri("https://gregtech.overminddl1.com/")
@@ -178,8 +192,11 @@ repositories {
178192
dependencies {
179193
annotationProcessor("com.github.bsideup.jabel:jabel-javac-plugin:0.4.2")
180194
compileOnly("com.github.bsideup.jabel:jabel-javac-plugin:0.4.2")
195+
// JMH compile task also runs javac; ensure Jabel plugin + runtime deps are available there too.
196+
add("jmhAnnotationProcessor", "com.github.bsideup.jabel:jabel-javac-plugin:0.4.2")
181197
// workaround for https://github.com/bsideup/jabel/issues/174
182198
annotationProcessor("net.java.dev.jna:jna-platform:5.13.0")
199+
add("jmhAnnotationProcessor", "net.java.dev.jna:jna-platform:5.13.0")
183200
// Allow jdk.unsupported classes like sun.misc.Unsafe, workaround for JDK-8206937 and fixes Forge crashes in tests.
184201
patchedMinecraft("me.eigenraven.java8unsupported:java-8-unsupported-shim:1.0.0")
185202
// allow Jabel to work in tests
@@ -204,7 +221,8 @@ dependencies {
204221

205222
// Mod Dependencies
206223
implementation("com.cleanroommc:configanytime:2.0")
207-
compileOnly("CraftTweaker2:CraftTweaker2-MC1120-Main:1.12-4.+")
224+
//compileOnly("CraftTweaker2:CraftTweaker2-MC1120-Main:1.12-4.1.20.711") compileOnly("CraftTweaker2:ZenScript:4.1.20.711")
225+
compileOnly("CraftTweaker2:CraftTweaker2-API:4.1.20.711")
208226
compileOnly(rfg.deobf("curse.maven:modularmachinery-community-edition-817377:5375642"))
209227
implementation(rfg.deobf("curse.maven:had-enough-items-557549:5210315"))
210228
compileOnly(rfg.deobf("curse.maven:jei-utilities-616190:4630499"))
@@ -298,6 +316,8 @@ dependencies {
298316
compileOnly(rfg.deobf("curse.maven:railcraft-51195:3853491"))
299317
compileOnly(rfg.deobf("curse.maven:deep-blood-evolution-836009:4690550"))
300318
compileOnly(rfg.deobf("curse.maven:tatw-263980:2585616"))
319+
compileOnly(rfg.deobf("curse.maven:resource-loader-226447:2477566"))
320+
compileOnly(rfg.deobf("curse.maven:base-246996:3440963"))
301321
}
302322

303323
// IDE Settings
@@ -348,4 +368,27 @@ idea {
348368

349369
tasks.processIdeaSettings.configure {
350370
dependsOn(tasks.injectTags)
351-
}
371+
}
372+
373+
// Allow narrowing JMH runs from command line, e.g.:
374+
// ./gradlew jmh -PjmhInclude=ParallelModelLoader
375+
// (value can be a full regex supported by JMH)
376+
jmh {
377+
val include = (project.findProperty("jmhInclude") as? String)?.trim().orEmpty()
378+
if (include.isNotEmpty()) {
379+
includes.set(listOf(include))
380+
}
381+
382+
// Enable JMH profilers from command line, e.g.:
383+
// ./gradlew jmh -PjmhInclude=... -PjmhProfilers=gc
384+
// ./gradlew jmh -PjmhProfilers=gc,stack
385+
val profilersProp = (project.findProperty("jmhProfilers") as? String)?.trim().orEmpty()
386+
if (profilersProp.isNotEmpty()) {
387+
profilersProp.split(',')
388+
.map { it.trim() }
389+
.filter { it.isNotEmpty() }
390+
.forEach { profiler ->
391+
profilers.add(profiler)
392+
}
393+
}
394+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package github.kasuminova.stellarcore.jmh;
2+
3+
import github.kasuminova.stellarcore.shaded.org.jctools.maps.NonBlockingHashMap;
4+
import github.kasuminova.stellarcore.shaded.org.jctools.maps.NonBlockingIdentityHashMap;
5+
import org.openjdk.jmh.annotations.Benchmark;
6+
import org.openjdk.jmh.annotations.BenchmarkMode;
7+
import org.openjdk.jmh.annotations.Fork;
8+
import org.openjdk.jmh.annotations.Level;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Param;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import org.openjdk.jmh.annotations.Threads;
17+
import org.openjdk.jmh.annotations.Warmup;
18+
import org.openjdk.jmh.infra.Blackhole;
19+
import org.openjdk.jmh.infra.ThreadParams;
20+
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.ConcurrentMap;
23+
import java.util.concurrent.TimeUnit;
24+
25+
/**
26+
* Map micro-benchmark for StellarCore's ParallelModelLoader-related workloads.
27+
*
28+
* Workload model / 负载模型:
29+
* - read-only: get(hit)
30+
* - write-only: put(update existing)
31+
* - mixed: get(hit) + put(update existing)
32+
* - keys are per-thread sharded to reduce unrealistic hot-key contention
33+
* - writes alternate between two pre-allocated values to force real updates
34+
*
35+
* Key modes / Key 形态:
36+
* - IDENTITY: keys are plain Objects (models often behave like identity keys)
37+
* - RESOURCE_LIKE: keys mimic ResourceLocation-style equals/hash (canonical instances)
38+
*
39+
* Note: NonBlockingIdentityHashMap is only semantically valid when keys are canonicalized
40+
* (i.e., the same instance is used for lookups). This benchmark keeps keys canonical.
41+
*/
42+
@BenchmarkMode(Mode.Throughput)
43+
@OutputTimeUnit(TimeUnit.SECONDS)
44+
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
45+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
46+
@Fork(1)
47+
public class ParallelModelLoaderMapJmhBenchmark {
48+
49+
public enum MapImpl {
50+
CHM,
51+
NBHM,
52+
NBIHM
53+
}
54+
55+
public enum KeyMode {
56+
IDENTITY,
57+
RESOURCE_LIKE
58+
}
59+
60+
@State(Scope.Benchmark)
61+
public static class MapState {
62+
63+
@Param({"CHM", "NBHM", "NBIHM"})
64+
public MapImpl mapImpl;
65+
66+
@Param({"IDENTITY", "RESOURCE_LIKE"})
67+
public KeyMode keyMode;
68+
69+
/** Must be >= threadCount, large enough to reduce contention. */
70+
@Param({"16384", "65536"})
71+
public int keyCount;
72+
73+
public Object[] keys;
74+
public Object[] valuesA;
75+
public Object[] valuesB;
76+
public ConcurrentMap<Object, Object> map;
77+
78+
@Setup(Level.Iteration)
79+
public void setupIteration() {
80+
if (keyCount <= 0) {
81+
throw new IllegalArgumentException("keyCount must be > 0");
82+
}
83+
84+
keys = new Object[keyCount];
85+
valuesA = new Object[keyCount];
86+
valuesB = new Object[keyCount];
87+
88+
switch (keyMode) {
89+
case IDENTITY:
90+
for (int i = 0; i < keyCount; i++) {
91+
keys[i] = new Object();
92+
}
93+
break;
94+
case RESOURCE_LIKE:
95+
for (int i = 0; i < keyCount; i++) {
96+
keys[i] = new ResourceKey(i);
97+
}
98+
break;
99+
default:
100+
throw new AssertionError(keyMode);
101+
}
102+
103+
for (int i = 0; i < keyCount; i++) {
104+
// Pre-allocate values to avoid measuring allocation/GC.
105+
// We alternate between A/B to ensure put() performs a real update.
106+
valuesA[i] = new Object();
107+
valuesB[i] = new Object();
108+
}
109+
110+
map = createMap(mapImpl, keyCount);
111+
for (int i = 0; i < keyCount; i++) {
112+
map.put(keys[i], valuesA[i]);
113+
}
114+
}
115+
116+
@SuppressWarnings({"rawtypes", "unchecked"})
117+
private static ConcurrentMap<Object, Object> createMap(final MapImpl impl, final int expectedSize) {
118+
final int initialCapacity = (int) (expectedSize / 0.75f) + 1;
119+
switch (impl) {
120+
case CHM:
121+
return new ConcurrentHashMap<>(initialCapacity);
122+
case NBHM:
123+
return (ConcurrentMap) new NonBlockingHashMap(expectedSize);
124+
case NBIHM:
125+
return (ConcurrentMap) new NonBlockingIdentityHashMap(expectedSize);
126+
default:
127+
throw new AssertionError(impl);
128+
}
129+
}
130+
}
131+
132+
@State(Scope.Thread)
133+
public static class ThreadState {
134+
135+
private int cursor;
136+
private int writeToggle;
137+
private int sliceOffset;
138+
private int sliceSize;
139+
private int sliceMask;
140+
141+
@Setup(Level.Iteration)
142+
public void setupIteration(final MapState mapState, final ThreadParams params) {
143+
cursor = 0;
144+
writeToggle = 0;
145+
146+
final int threads = Math.max(1, params.getThreadCount());
147+
int perThread = mapState.keyCount / threads;
148+
if (perThread <= 0) {
149+
perThread = 1;
150+
}
151+
152+
sliceSize = perThread;
153+
sliceOffset = perThread * params.getThreadIndex();
154+
155+
// use bitmask when possible, otherwise fall back to modulo
156+
sliceMask = (Integer.bitCount(sliceSize) == 1) ? (sliceSize - 1) : -1;
157+
158+
// Defensive clamp: in weird configurations (threads > keyCount) sliceOffset can overflow keyCount.
159+
if (sliceOffset >= mapState.keyCount) {
160+
sliceOffset = sliceOffset % mapState.keyCount;
161+
}
162+
}
163+
164+
public int nextIndex(final MapState mapState) {
165+
final int i = cursor++;
166+
final int inSlice = (sliceMask != -1) ? (i & sliceMask) : (i % sliceSize);
167+
final int idx = sliceOffset + inSlice;
168+
return (idx < mapState.keyCount) ? idx : (idx % mapState.keyCount);
169+
}
170+
171+
public Object nextWriteValue(final MapState mapState, final int idx) {
172+
writeToggle ^= 1;
173+
return writeToggle == 0 ? mapState.valuesA[idx] : mapState.valuesB[idx];
174+
}
175+
}
176+
177+
@Benchmark
178+
@Threads(1)
179+
public void singleThread_get_hit(final MapState state, final ThreadState thread, final Blackhole bh) {
180+
final int idx = thread.nextIndex(state);
181+
final Object key = state.keys[idx];
182+
183+
bh.consume(state.map.get(key));
184+
}
185+
186+
@Benchmark
187+
@Threads(Threads.MAX)
188+
public void concurrent_get_hit(final MapState state, final ThreadState thread, final Blackhole bh) {
189+
final int idx = thread.nextIndex(state);
190+
final Object key = state.keys[idx];
191+
192+
bh.consume(state.map.get(key));
193+
}
194+
195+
@Benchmark
196+
@Threads(1)
197+
public void singleThread_put_updateExisting(final MapState state, final ThreadState thread, final Blackhole bh) {
198+
final int idx = thread.nextIndex(state);
199+
final Object key = state.keys[idx];
200+
final Object value = thread.nextWriteValue(state, idx);
201+
202+
bh.consume(state.map.put(key, value));
203+
}
204+
205+
@Benchmark
206+
@Threads(Threads.MAX)
207+
public void concurrent_put_updateExisting(final MapState state, final ThreadState thread, final Blackhole bh) {
208+
final int idx = thread.nextIndex(state);
209+
final Object key = state.keys[idx];
210+
final Object value = thread.nextWriteValue(state, idx);
211+
212+
bh.consume(state.map.put(key, value));
213+
}
214+
215+
@Benchmark
216+
@Threads(1)
217+
public void singleThread_getThenPut_mixed(final MapState state, final ThreadState thread, final Blackhole bh) {
218+
final int idx = thread.nextIndex(state);
219+
final Object key = state.keys[idx];
220+
final Object value = thread.nextWriteValue(state, idx);
221+
222+
bh.consume(state.map.get(key));
223+
bh.consume(state.map.put(key, value));
224+
}
225+
226+
@Benchmark
227+
@Threads(Threads.MAX)
228+
public void concurrent_getThenPut_mixed(final MapState state, final ThreadState thread, final Blackhole bh) {
229+
final int idx = thread.nextIndex(state);
230+
final Object key = state.keys[idx];
231+
final Object value = thread.nextWriteValue(state, idx);
232+
233+
bh.consume(state.map.get(key));
234+
bh.consume(state.map.put(key, value));
235+
}
236+
237+
private static final class ResourceKey {
238+
private final String namespace;
239+
private final String path;
240+
private final int hash;
241+
242+
ResourceKey(final int id) {
243+
// Small namespace space (like many mods sharing a few namespaces), unique paths.
244+
this.namespace = "mod" + (id & 1023);
245+
this.path = "model/" + id;
246+
247+
int h = namespace.hashCode();
248+
h = 31 * h + path.hashCode();
249+
this.hash = h;
250+
}
251+
252+
@Override
253+
public int hashCode() {
254+
return hash;
255+
}
256+
257+
@Override
258+
public boolean equals(final Object obj) {
259+
if (this == obj) {
260+
return true;
261+
}
262+
if (!(obj instanceof ResourceKey)) {
263+
return false;
264+
}
265+
final ResourceKey other = (ResourceKey) obj;
266+
return namespace.equals(other.namespace) && path.equals(other.path);
267+
}
268+
}
269+
}

0 commit comments

Comments
 (0)