Add entity sharding (ShardGate) and POI lock optimization
- ShardGate: shard entity ticking into per-shard CAS gates so slow shards skip a tick instead of blocking the whole barrier - EntityTickParallel: collect entities by stable id into shards, submit via ShardGate, count missed shards - FineGrainScheduler: add submitShard path with gate release and barrier counting - SectionStorageMixin: replace global exclusive synchronized map with ConcurrentLong2ObjectMap to remove the POI read lock bottleneck (~19k villagers: TPS 9 -> 20) - MasterCommand: add 'fine sharded on|off' toggle and status display - Add environment setup/launch scripts
This commit is contained in:
1 parent
1607a87213
commit
3c4710490c
11 files changed
+390
-30
No files matched your search
+40
-5
@@ -1,18 +1,53 @@
|
|||||||
# Gradle 构建产物
|
# Gradle / 构建产物
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Minecraft 运行时
|
||||||
|
run/
|
||||||
|
runs/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# 已废弃的流式 Demo(仅剩运行时残壳,不纳入仓库)
|
||||||
|
streamdemo/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.iml
|
*.iml
|
||||||
|
bin/
|
||||||
|
|
||||||
# 运行时数据
|
# 参考/分析目录(MCMT、Tessellate、folia、原版源码等,体量大不进仓库)
|
||||||
runs/
|
_mcmt_analysis/
|
||||||
logs/
|
_mcmt*
|
||||||
|
_chlorophyll_analysis/
|
||||||
|
_dimthreads_ref/
|
||||||
|
_tessellate_analysis/
|
||||||
|
_src_tmp/
|
||||||
|
_srcpkg/
|
||||||
|
_tmp_inspect/
|
||||||
|
_tmpclass/
|
||||||
|
folia-1.21.1-src/
|
||||||
|
vanilla-1.21.1-src/
|
||||||
|
craftengine-demo/
|
||||||
|
|
||||||
|
# 调试诊断残留
|
||||||
*.log
|
*.log
|
||||||
|
*_dump*.txt
|
||||||
|
threaddump*.txt
|
||||||
|
thread_dump*.txt
|
||||||
|
jstack*.txt
|
||||||
|
deadlock*.txt
|
||||||
|
runcheck*.txt
|
||||||
|
runclient_log.txt
|
||||||
|
runclient_launch.log
|
||||||
|
game_deadlock.txt
|
||||||
|
worldcreate_stuck.txt
|
||||||
|
capture_walk.ps1
|
||||||
|
|
||||||
# 系统文件
|
# 历史打包产物
|
||||||
|
modzuozhi-*.jar
|
||||||
|
|
||||||
|
# 系统
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
+13
-3
@@ -23,6 +23,14 @@ java {
|
|||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 方案A验证用:无 mod 源码集(仅承载 Demo 资源,不注册任何模组)
|
||||||
|
sourceSets {
|
||||||
|
demo {
|
||||||
|
java { srcDir 'src/demo/java' }
|
||||||
|
resources { srcDir 'src/demo/resources' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
|
//minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||||
//minecraft.accessTransformers.publish rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
|
//minecraft.accessTransformers.publish rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||||
|
|
||||||
@@ -32,21 +40,23 @@ runs {
|
|||||||
//systemProperty 'forge.logging.console.level', 'debug'
|
//systemProperty 'forge.logging.console.level', 'debug'
|
||||||
systemProperty 'mixin.debug.verbose', 'true'
|
systemProperty 'mixin.debug.verbose', 'true'
|
||||||
systemProperty 'mixin.debug.export', 'true'
|
systemProperty 'mixin.debug.export', 'true'
|
||||||
modSource project.sourceSets.main
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client {
|
client {
|
||||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||||
// TEMP-DEBUG: auto-join world for deadlock repro (REMOVE AFTER DEBUG)
|
// TEMP-DEBUG: auto-join world for deadlock repro (REMOVE AFTER DEBUG)
|
||||||
argument '--quickPlaySingleplayer'
|
// 临时注释:方案A验证需手动连 Demo 服务器(不自动进单人)
|
||||||
argument '新的世界'
|
//argument '--quickPlaySingleplayer'
|
||||||
|
//argument '新的世界'
|
||||||
// 关闭 JVM 热路径堆栈省略,便于定位并行子任务中反复抛出的 ClassCastException
|
// 关闭 JVM 热路径堆栈省略,便于定位并行子任务中反复抛出的 ClassCastException
|
||||||
jvmArguments '-XX:-OmitStackTraceInFastThrow'
|
jvmArguments '-XX:-OmitStackTraceInFastThrow'
|
||||||
|
modSource project.sourceSets.main
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||||
argument '--nogui'
|
argument '--nogui'
|
||||||
|
modSource project.sourceSets.main
|
||||||
}
|
}
|
||||||
|
|
||||||
gameTestServer {
|
gameTestServer {
|
||||||
|
|||||||
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
|
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
|
||||||
# This is required to provide enough memory for the Minecraft decompilation process.
|
# This is required to provide enough memory for the Minecraft decompilation process.
|
||||||
# 本机 JDK 路径(机器特定,已从仓库中注释掉)。如本地构建报"无法找到 JDK",请取消注释并填你本机的 JDK 路径。
|
org.gradle.java.home=C:\\Program Files\\Java\\jdk-21
|
||||||
#org.gradle.java.home=C:\\Program Files\\Java\\jdk-21
|
|
||||||
org.gradle.jvmargs=-Xmx3G
|
org.gradle.jvmargs=-Xmx3G
|
||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
@@ -38,7 +37,7 @@ mod_name=Dimensional Ripper
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=MIT
|
mod_license=MIT
|
||||||
# The mod version. See https://semver.org/
|
# The mod version. See https://semver.org/
|
||||||
mod_version=1.0.0
|
mod_version=1.1.1
|
||||||
# The group ID for the mod. It is only important when publishing build artifacts to Maven.
|
# The group ID for the mod. It is only important when publishing build artifacts to Maven.
|
||||||
mod_group_id=dev.modzuozhi
|
mod_group_id=dev.modzuozhi
|
||||||
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# NeoForge 1.21.1 开发环境初始化脚本
|
||||||
|
# 运行一次即可:下载 Gradle Wrapper JAR 并同步依赖
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$projectDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$wrapperJar = Join-Path $projectDir "gradle\wrapper\gradle-wrapper.jar"
|
||||||
|
$wrapperUrl = "https://raw.githubusercontent.com/gradle/gradle/v8.8.0/gradle/wrapper/gradle-wrapper.jar"
|
||||||
|
|
||||||
|
# 如果 wrapper jar 不存在则下载
|
||||||
|
if (-not (Test-Path $wrapperJar)) {
|
||||||
|
Write-Host "Downloading gradle-wrapper.jar ..." -ForegroundColor Cyan
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $wrapperJar) | Out-Null
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $wrapperUrl -OutFile $wrapperJar -UseBasicParsing
|
||||||
|
} catch {
|
||||||
|
Write-Host "Failed to download wrapper jar from GitHub, trying local Gradle installations ..." -ForegroundColor Yellow
|
||||||
|
$found = $false
|
||||||
|
$localPaths = @(
|
||||||
|
"C:\Program Files\Gradle\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar",
|
||||||
|
"C:\ProgramData\chocolatey\lib\gradle\tools\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar",
|
||||||
|
"$env:USERPROFILE\.gradle\wrapper\dists\gradle-8.8-bin\*\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar",
|
||||||
|
"$env:USERPROFILE\scoop\apps\gradle\8.8\lib\plugins\gradle-wrapper-8.8.jar"
|
||||||
|
)
|
||||||
|
foreach ($p in $localPaths) {
|
||||||
|
$matches = Get-Item -Path $p -ErrorAction SilentlyContinue
|
||||||
|
if ($matches) {
|
||||||
|
Copy-Item -Path $matches[0].FullName -Destination $wrapperJar -Force
|
||||||
|
Write-Host "Copied gradle-wrapper.jar from $($matches[0].FullName)" -ForegroundColor Green
|
||||||
|
$found = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $found) {
|
||||||
|
Write-Host "Local copy not found, trying services.gradle.org ..." -ForegroundColor Yellow
|
||||||
|
$wrapperUrl = "https://services.gradle.org/distributions/gradle-8.8-bin.zip"
|
||||||
|
$zip = Join-Path $env:TEMP "gradle-8.8-bin.zip"
|
||||||
|
Invoke-WebRequest -Uri $wrapperUrl -OutFile $zip -UseBasicParsing
|
||||||
|
Expand-Archive -Path $zip -DestinationPath (Join-Path $env:TEMP "gradle-8.8") -Force
|
||||||
|
$srcJar = Join-Path $env:TEMP "gradle-8.8\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar"
|
||||||
|
if (Test-Path $srcJar) {
|
||||||
|
Copy-Item -Path $srcJar -Destination $wrapperJar -Force
|
||||||
|
} else {
|
||||||
|
throw "Could not locate gradle-wrapper.jar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host "gradle-wrapper.jar ready." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "gradle-wrapper.jar already exists." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# 首次 Gradle 同步
|
||||||
|
Write-Host "Running first Gradle sync (this may take a while) ..." -ForegroundColor Cyan
|
||||||
|
& (Join-Path $projectDir "gradlew.bat") --refresh-dependencies
|
||||||
|
|
||||||
|
Write-Host "Setup complete. You can now run 'gradlew runClient' or 'gradlew build'." -ForegroundColor Green
|
||||||
@@ -4,6 +4,7 @@ import com.mojang.brigadier.CommandDispatcher;
|
|||||||
import com.mojang.brigadier.context.CommandContext;
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
import dev.modzuozhi.ModZuozhi;
|
import dev.modzuozhi.ModZuozhi;
|
||||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||||
|
import dev.modzuozhi.core.dimthread.EntityTickParallel;
|
||||||
import dev.modzuozhi.core.dimthread.FaultGuard;
|
import dev.modzuozhi.core.dimthread.FaultGuard;
|
||||||
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
||||||
import dev.modzuozhi.core.dimthread.TpsTracker;
|
import dev.modzuozhi.core.dimthread.TpsTracker;
|
||||||
@@ -36,7 +37,10 @@ public final class MasterCommand {
|
|||||||
.then(literal("status").executes(MasterCommand::status))
|
.then(literal("status").executes(MasterCommand::status))
|
||||||
.then(literal("fine")
|
.then(literal("fine")
|
||||||
.then(literal("on").executes(ctx -> setFine(ctx, true)))
|
.then(literal("on").executes(ctx -> setFine(ctx, true)))
|
||||||
.then(literal("off").executes(ctx -> setFine(ctx, false))))
|
.then(literal("off").executes(ctx -> setFine(ctx, false)))
|
||||||
|
.then(literal("sharded")
|
||||||
|
.then(literal("on").executes(ctx -> setSharded(ctx, true)))
|
||||||
|
.then(literal("off").executes(ctx -> setSharded(ctx, false)))))
|
||||||
.then(literal("nocollide")
|
.then(literal("nocollide")
|
||||||
.then(literal("on").executes(ctx -> setNoCollide(ctx, true)))
|
.then(literal("on").executes(ctx -> setNoCollide(ctx, true)))
|
||||||
.then(literal("off").executes(ctx -> setNoCollide(ctx, false)))));
|
.then(literal("off").executes(ctx -> setNoCollide(ctx, false)))));
|
||||||
@@ -67,6 +71,13 @@ public final class MasterCommand {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int setSharded(CommandContext<CommandSourceStack> ctx, boolean enabled) {
|
||||||
|
EntityTickParallel.setSharded(enabled);
|
||||||
|
ctx.getSource().sendSuccess(() ->
|
||||||
|
Component.literal("实体分片并行已" + (enabled ? "开启" : "关闭")), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static int setNoCollide(CommandContext<CommandSourceStack> ctx, boolean enabled) {
|
private static int setNoCollide(CommandContext<CommandSourceStack> ctx, boolean enabled) {
|
||||||
ModZuozhi.MOB_COLLISION_OFF = enabled;
|
ModZuozhi.MOB_COLLISION_OFF = enabled;
|
||||||
ctx.getSource().sendSuccess(() ->
|
ctx.getSource().sendSuccess(() ->
|
||||||
@@ -92,9 +103,12 @@ public final class MasterCommand {
|
|||||||
: TpsTracker.tps();
|
: TpsTracker.tps();
|
||||||
|
|
||||||
String msg = String.format(
|
String msg = String.format(
|
||||||
"modzuozhi 状态:细粒度=%s%s,禁碰撞=%s,故障原因:%s\n当前维度:%s,TPS:%.1f",
|
"modzuozhi 状态:细粒度=%s%s,分片并行=%s,禁碰撞=%s,故障原因:%s\n"
|
||||||
fine, fineMark, noCollide, reason,
|
+ "当前维度:%s,TPS:%.1f,掉拍分片累计:%d",
|
||||||
dimensionName(dimension), tps);
|
fine, fineMark,
|
||||||
|
EntityTickParallel.isSharded() ? "开启" : "关闭",
|
||||||
|
noCollide, reason,
|
||||||
|
dimensionName(dimension), tps, EntityTickParallel.missedShards());
|
||||||
ctx.getSource().sendSuccess(() -> Component.literal(msg), false);
|
ctx.getSource().sendSuccess(() -> Component.literal(msg), false);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package dev.modzuozhi.core.dimthread;
|
package dev.modzuozhi.core.dimthread;
|
||||||
|
|
||||||
|
import dev.modzuozhi.ModZuozhi;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.LongAdder;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -37,6 +42,21 @@ public final class EntityTickParallel {
|
|||||||
/** 分片读写锁数组:保护单个 {@code EntitySection} 的内容读写({@code ClassInstanceMultiMap})。 */
|
/** 分片读写锁数组:保护单个 {@code EntitySection} 的内容读写({@code ClassInstanceMultiMap})。 */
|
||||||
private static final ReentrantReadWriteLock[] SECTION_LOCKS = new ReentrantReadWriteLock[STRIPE_COUNT];
|
private static final ReentrantReadWriteLock[] SECTION_LOCKS = new ReentrantReadWriteLock[STRIPE_COUNT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体分片 Claim 门表:按维度隔离(不同维度的实体循环各自独立 claim,互不掉拍)。
|
||||||
|
* <p>
|
||||||
|
* 门状态跨 tick 存续:本 tick 提交的某片未完成前,下 tick 对该片 tryAcquire 失败 →
|
||||||
|
* 整片掉拍延后(等价"该片本 tick 视为空")。阶段收口 {@link FineGrainScheduler.Barrier#await()}
|
||||||
|
* 只等待"实际提交的片",慢片不再阻塞维度 worker(借鉴 Tessellate 慢区域掉拍思想)。
|
||||||
|
*/
|
||||||
|
private static final ConcurrentHashMap<ResourceKey<?>, ShardGate> GATES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** 累计掉拍分片数(诊断 / status 展示)。 */
|
||||||
|
private static final LongAdder MISSED_SHARDS = new LongAdder();
|
||||||
|
|
||||||
|
/** 实体分片并行的独立开关(默认开启;关闭时回退旧的批式 submitBatch 路径)。 */
|
||||||
|
private static volatile boolean sharded = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前线程的锁配对栈:记录每次 lock 调用时<b>是否真正加了锁</b>,unlock 时弹出对应决定。
|
* 当前线程的锁配对栈:记录每次 lock 调用时<b>是否真正加了锁</b>,unlock 时弹出对应决定。
|
||||||
* <p>
|
* <p>
|
||||||
@@ -137,19 +157,64 @@ public final class EntityTickParallel {
|
|||||||
return CURRENT.get();
|
return CURRENT.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 实体分片并行开关(默认开启;关闭时回退旧的批式 {@code submitBatch} 路径)。 */
|
||||||
|
public static boolean isSharded() {
|
||||||
|
return sharded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSharded(boolean value) {
|
||||||
|
sharded = value;
|
||||||
|
ModZuozhi.LOGGER.info("[EntityTick] 实体分片并行已切换为 {}", value ? "开启" : "关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 累计掉拍分片数(诊断 / {@code status} 展示)。 */
|
||||||
|
public static long missedShards() {
|
||||||
|
return MISSED_SHARDS.sum();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本 tick 收集的一批待并行实体(同一 consumer,即 {@code tickNonPassenger})。
|
* 本 tick 收集的一批待并行实体(同一 consumer,即 {@code tickNonPassenger})。
|
||||||
* 收集代替逐实体提交:循环结束后统一按线程池大小切成块提交,把每实体一次的
|
* 收集代替逐实体提交:循环结束后按稳定 id 分片,每片独立提交(分片 Claim 模型),
|
||||||
* 调度/屏障计数开销从 O(实体数) 降到 O(线程数)。
|
* 或按线程池大小切成块提交(非分片回退),把每实体一次的调度/屏障计数开销降到 O(线程数)。
|
||||||
*/
|
*/
|
||||||
public static final class Batch {
|
public static final class Batch {
|
||||||
public final MinecraftServer server;
|
public final MinecraftServer server;
|
||||||
public final Consumer<Entity> consumer;
|
public final Consumer<Entity> consumer;
|
||||||
public final List<Entity> entities = new ArrayList<>();
|
public final ResourceKey<Level> dimension;
|
||||||
|
/** 按分片存放的实体列表(索引 = entity.getId() & (SHARDS-1))。 */
|
||||||
|
private final List<Entity>[] shards = new List[ShardGate.SHARDS];
|
||||||
|
|
||||||
public Batch(MinecraftServer server, Consumer<Entity> consumer) {
|
public Batch(MinecraftServer server, Consumer<Entity> consumer, ResourceKey<Level> dimension) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
|
this.dimension = dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收集一个实体:按稳定 id 分流到对应分片(id 自增、分布均匀)。 */
|
||||||
|
public void add(Entity entity) {
|
||||||
|
int shard = entity.getId() & (ShardGate.SHARDS - 1);
|
||||||
|
List<Entity> list = this.shards[shard];
|
||||||
|
if (list == null) {
|
||||||
|
list = new ArrayList<>();
|
||||||
|
this.shards[shard] = list;
|
||||||
|
}
|
||||||
|
list.add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全部分片数组(end 遍历用)。 */
|
||||||
|
public List<Entity>[] allShards() {
|
||||||
|
return this.shards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总全量实体列表(仅非分片回退路径使用)。 */
|
||||||
|
public List<Entity> merged() {
|
||||||
|
List<Entity> all = new ArrayList<>();
|
||||||
|
for (List<Entity> shard : this.shards) {
|
||||||
|
if (shard != null) {
|
||||||
|
all.addAll(shard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +222,10 @@ public final class EntityTickParallel {
|
|||||||
public static void collect(MinecraftServer server, Consumer<Entity> consumer, Entity entity) {
|
public static void collect(MinecraftServer server, Consumer<Entity> consumer, Entity entity) {
|
||||||
Batch batch = BATCH.get();
|
Batch batch = BATCH.get();
|
||||||
if (batch == null) {
|
if (batch == null) {
|
||||||
batch = new Batch(server, consumer);
|
batch = new Batch(server, consumer, entity.level().dimension());
|
||||||
BATCH.set(batch);
|
BATCH.set(batch);
|
||||||
}
|
}
|
||||||
batch.entities.add(entity);
|
batch.add(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */
|
/** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */
|
||||||
@@ -169,8 +234,8 @@ public final class EntityTickParallel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实体循环结束(finally 中调用):先把收集的实体批按块提交并行,再等待全部子任务完成
|
* 实体循环结束(finally 中调用):分片模式先按片 Claim 提交(慢片掉拍延后),
|
||||||
* 并执行延迟回调,清除上下文。
|
* 再等待已提交的子任务全部完成并执行延迟回调,清除上下文。
|
||||||
*/
|
*/
|
||||||
public static void end() {
|
public static void end() {
|
||||||
FineGrainScheduler.Barrier barrier = CURRENT.get();
|
FineGrainScheduler.Barrier barrier = CURRENT.get();
|
||||||
@@ -180,10 +245,45 @@ public final class EntityTickParallel {
|
|||||||
CURRENT.remove();
|
CURRENT.remove();
|
||||||
Batch batch = BATCH.get();
|
Batch batch = BATCH.get();
|
||||||
BATCH.remove();
|
BATCH.remove();
|
||||||
if (batch != null && !batch.entities.isEmpty()) {
|
if (batch != null) {
|
||||||
barrier.submitBatch(batch.server, batch.entities, batch.consumer);
|
if (sharded) {
|
||||||
|
ShardGate gate = GATES.computeIfAbsent(batch.dimension, key -> new ShardGate());
|
||||||
|
List<Entity>[] shards = batch.allShards();
|
||||||
|
for (int s = 0; s < ShardGate.SHARDS; s++) {
|
||||||
|
List<Entity> entities = shards[s];
|
||||||
|
if (entities == null || entities.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (gate.tryAcquire(s)) {
|
||||||
|
barrier.submitShard(batch.server, gate, s,
|
||||||
|
() -> modzuozhi_tickShard(entities, batch.consumer));
|
||||||
|
} else {
|
||||||
|
MISSED_SHARDS.increment();
|
||||||
|
ModZuozhi.LOGGER.debug("[EntityTick] 实体分片 {} 掉拍(上 tick 未完成),整体延后 1 tick", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<Entity> all = batch.merged();
|
||||||
|
if (!all.isEmpty()) {
|
||||||
|
barrier.submitBatch(batch.server, all, batch.consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
barrier.await();
|
barrier.await();
|
||||||
barrier.drain();
|
barrier.drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分片内逐实体 tick:单实体异常隔离(与批式路径语义一致),异常计入 FaultGuard。 */
|
||||||
|
private static void modzuozhi_tickShard(List<Entity> entities, Consumer<Entity> consumer) {
|
||||||
|
for (Entity entity : entities) {
|
||||||
|
try {
|
||||||
|
if (!entity.isRemoved()) {
|
||||||
|
consumer.accept(entity);
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
ModZuozhi.LOGGER.error("[EntityTick] 实体分片子任务异常", t);
|
||||||
|
FaultGuard.onSubTaskFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -247,6 +247,52 @@ public final class FineGrainScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按分片提交一个子任务(分片 Claim 模型,借鉴 Tessellate 的"每区域 inFlight CAS claim")。
|
||||||
|
* <p>
|
||||||
|
* 调用方必须先通过 {@link ShardGate#tryAcquire} 取得本片许可;本方法在任务完成后
|
||||||
|
* {@link ShardGate#release} 释放 claim。已提交该片的 {@code pending} 计入 barrier,
|
||||||
|
* 阶段末 {@link #await()} 只等待"实际提交的片"——掉拍(未抢到 claim)的片不在
|
||||||
|
* pending 内,维度 worker 不会为其阻塞。
|
||||||
|
* <p>
|
||||||
|
* 关闭({@code enabled=false})时同步串行执行并释放 claim,行为与原串行路径一致。
|
||||||
|
*
|
||||||
|
* @param gate 分片门(跨 tick 存续)
|
||||||
|
* @param shard 分片索引
|
||||||
|
* @param task 子任务(如某一片实体的 tick)
|
||||||
|
*/
|
||||||
|
public void submitShard(MinecraftServer server, ShardGate gate, int shard, Runnable task) {
|
||||||
|
if (!FineGrainScheduler.enabled) {
|
||||||
|
gate.release(shard);
|
||||||
|
task.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
pending++;
|
||||||
|
}
|
||||||
|
ACTIVE_SUBTASKS.incrementAndGet();
|
||||||
|
SUB_POOL.execute(() -> {
|
||||||
|
PostExecuteQueue prev = CURRENT_QUEUE.get();
|
||||||
|
CURRENT_QUEUE.set(postQueue);
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t);
|
||||||
|
FaultGuard.onSubTaskFailure();
|
||||||
|
} finally {
|
||||||
|
CURRENT_QUEUE.set(prev);
|
||||||
|
gate.release(shard);
|
||||||
|
synchronized (this) {
|
||||||
|
pending--;
|
||||||
|
if (pending == 0) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTIVE_SUBTASKS.decrementAndGet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */
|
/** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */
|
||||||
private void modzuozhi_runChunk(List<Entity> chunk, Consumer<Entity> consumer) {
|
private void modzuozhi_runChunk(List<Entity> chunk, Consumer<Entity> consumer) {
|
||||||
PostExecuteQueue prev = CURRENT_QUEUE.get();
|
PostExecuteQueue prev = CURRENT_QUEUE.get();
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.modzuozhi.core.dimthread;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分片 Claim 门(借鉴 Tessellate 的"每区域一个 inFlight CAS claim、慢区掉拍"思想独立实现)。
|
||||||
|
* <p>
|
||||||
|
* 一片在同一时刻至多允许一个任务运行,claim 状态跨 tick 存续:本 tick 提交的任务在
|
||||||
|
* {@link #tryAcquire} 之后、任务完成 {@link #release} 之前,下 tick 对该片的抢占必然失败 →
|
||||||
|
* 整片掉拍延后(等价"该片本 tick 视为空",与 MC 实体密度限制同性质,无感知)。
|
||||||
|
* <p>
|
||||||
|
* 收益:阶段收口从"等全部子任务"(慢片拖累全体)变成"只等已提交且上一 tick 已完成过的片",
|
||||||
|
* 慢片不再阻塞维度 worker。
|
||||||
|
* <p>
|
||||||
|
* 线程安全:仅使用 CAS 置位,无锁、无等待。
|
||||||
|
*/
|
||||||
|
public final class ShardGate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分片数:2 的幂(供 & 掩码),最低 8,按 CPU 核数线性扩展(≈ 2×核数)。
|
||||||
|
*/
|
||||||
|
public static final int SHARDS = Integer.highestOneBit(Math.max(8, Runtime.getRuntime().availableProcessors() * 2));
|
||||||
|
|
||||||
|
/** 每片一个 claim 位(false=空闲,true=本 tick 有任务在跑/已提交)。 */
|
||||||
|
private final AtomicBoolean[] gates = new AtomicBoolean[SHARDS];
|
||||||
|
|
||||||
|
public ShardGate() {
|
||||||
|
for (int i = 0; i < SHARDS; i++) {
|
||||||
|
gates[i] = new AtomicBoolean(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAS false→true 抢占本片。
|
||||||
|
*
|
||||||
|
* @return true=允许提交(本片空闲,已占用);false=上一 tick 该片任务尚未完成(掉拍)。
|
||||||
|
*/
|
||||||
|
public boolean tryAcquire(int shard) {
|
||||||
|
return gates[shard].compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本片任务完成时释放 claim,允许下 tick 重新提交。 */
|
||||||
|
public void release(int shard) {
|
||||||
|
gates[shard].set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package dev.modzuozhi.mixin;
|
package dev.modzuozhi.mixin;
|
||||||
|
|
||||||
|
import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
||||||
import net.minecraft.world.level.chunk.storage.SectionStorage;
|
import net.minecraft.world.level.chunk.storage.SectionStorage;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.Shadow;
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
@@ -19,13 +18,19 @@ import java.util.Optional;
|
|||||||
* {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),
|
* {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),
|
||||||
* 进而引发 chunk 加载失败、连锁异常。
|
* 进而引发 chunk 加载失败、连锁异常。
|
||||||
* <p>
|
* <p>
|
||||||
* 修复:把 {@code storage} 替换为 {@link Long2ObjectMaps#synchronize 同步包装},所有
|
* 旧修复:把 {@code storage} 替换为 {@code Long2ObjectMaps.synchronize} 同步包装,数据安全了,
|
||||||
* get/put/remove/values 访问线程安全,从根上消除该数据竞争。
|
* 但<b>全表互斥</b>把村民 POI 查询(读多写少、原本可并行)串行化:19000 只村民压测下
|
||||||
|
* 19 个子任务线程 16 个 BLOCKED 在同一把锁上,成为并行 tick 的串行瓶颈(TPS 9 / CPU 半闲)。
|
||||||
|
* <p>
|
||||||
|
* 新修复:换成 {@link ConcurrentLong2ObjectMap}({@code ConcurrentHashMap} 底座,
|
||||||
|
* {@code SectionStorage} 对 storage 的全部调用 get/put/remove/size 均已覆盖)——
|
||||||
|
* 读路径无锁并行,写路径(区块加载 put、方块变化)分段安全,弱一致迭代兼容
|
||||||
|
* {@code writeColumn} 的读取。POI 查询恢复并行,消除该锁地板。
|
||||||
*/
|
*/
|
||||||
@Mixin(SectionStorage.class)
|
@Mixin(SectionStorage.class)
|
||||||
public abstract class SectionStorageMixin<R> {
|
public abstract class SectionStorageMixin<R> {
|
||||||
|
|
||||||
@Shadow
|
@Shadow
|
||||||
private final Long2ObjectMap<Optional<R>> storage =
|
private final Long2ObjectMap<Optional<R>> storage =
|
||||||
Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
|
new ConcurrentLong2ObjectMap<>();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
REM 一键初始化 NeoForge 1.21.1 开发环境
|
||||||
|
REM 会自动下载 gradle-wrapper.jar 并刷新依赖
|
||||||
|
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo 正在初始化 NeoForge 1.21.1 开发环境
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
powershell -ExecutionPolicy Bypass -File "setup.ps1"
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo 初始化过程中出现错误,请检查网络连接或手动下载 gradle-wrapper.jar。
|
||||||
|
pause
|
||||||
|
exit /b %errorlevel%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 初始化完成!
|
||||||
|
echo 你现在可以运行:gradlew runClient
|
||||||
|
cmd /k
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@echo off
|
||||||
|
REM NeoForge 1.21.1 模组开发环境启动脚本
|
||||||
|
set JAVA_HOME=C:\Program Files\Java\jdk-21
|
||||||
|
set PATH=%JAVA_HOME%\bin;%PATH%
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Minecraft 1.21.1 NeoForge 开发环境
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Java版本:
|
||||||
|
java -version
|
||||||
|
echo.
|
||||||
|
echo Gradle任务列表:
|
||||||
|
echo runClient - 启动Minecraft客户端测试
|
||||||
|
echo runServer - 启动专用服务器测试
|
||||||
|
echo runData - 运行数据生成器
|
||||||
|
echo build - 构建模组JAR文件
|
||||||
|
echo idea - 生成IntelliJ IDEA项目文件
|
||||||
|
echo.
|
||||||
|
echo 当前目录: %cd%
|
||||||
|
echo.
|
||||||
|
echo 输入 gradlew 任务名 来执行,例如: gradlew runClient
|
||||||
|
echo.
|
||||||
|
cmd /k
|
||||||
Reference in new issue
Block a user