9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-25 09:59:15 +00:00

optimize async target finding (#303)

* optimize async target finding

* fix canContinueToUse

* fix data race

* fix TemptGoal start before search entity

* fix AvoidEntityGoal doesn't create path

* fix inactiveTick tickingTarget

* default queueSize to 0

* default queueSize to 4096
This commit is contained in:
hayanesuru
2025-05-01 22:29:42 +08:00
committed by GitHub
parent 2b13b38801
commit 3b8ec970c8
6 changed files with 725 additions and 524 deletions

View File

@@ -1,19 +1,76 @@
package org.dreeam.leaf.async.ai;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dreeam.leaf.config.modules.async.AsyncTargetFinding;
import org.dreeam.leaf.util.queue.SpscIntQueue;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.LockSupport;
public class AsyncGoalExecutor {
public static ExecutorService EXECUTOR;
public static final Logger LOGGER = LogManager.getLogger("Leaf Async Goal");
final SpscIntQueue queue;
final SpscIntQueue wake;
private final AsyncGoalThread thread;
private final ServerLevel serverLevel;
private boolean dirty = false;
private long tickCount = 0L;
public static final Logger LOGGER = LogManager.getLogger("Leaf Async Entity Lookup");
public AsyncGoalExecutor(AsyncGoalThread thread, ServerLevel serverLevel) {
this.serverLevel = serverLevel;
queue = new SpscIntQueue(AsyncTargetFinding.queueSize);
wake = new SpscIntQueue(AsyncTargetFinding.queueSize);
this.thread = thread;
}
public static void runTasks(List<Runnable> tasks) {
for (Runnable task : tasks) {
task.run();
boolean wake(int id) {
Entity entity = this.serverLevel.getEntities().get(id);
if (entity == null || entity.isRemoved() || !(entity instanceof Mob m)) {
return false;
}
m.goalSelector.wake();
m.targetSelector.wake();
return true;
}
public final void submit(int entityId) {
if (!this.queue.send(entityId)) {
LockSupport.unpark(thread);
while (!this.queue.send(entityId)) {
Thread.onSpinWait();
}
}
dirty = true;
}
public final void unpark() {
if (dirty) LockSupport.unpark(thread);
dirty = false;
}
public final void midTick() {
while (true) {
int id = this.wake.recv();
if (id == Integer.MAX_VALUE) {
break;
}
Entity entity = this.serverLevel.getEntities().get(id);
if (entity == null || !entity.isAlive() || !(entity instanceof Mob mob)) {
continue;
}
mob.tickingTarget = true;
boolean a = mob.targetSelector.poll();
mob.tickingTarget = false;
boolean b = mob.goalSelector.poll();
if (a || b) {
submit(id);
}
}
if ((tickCount & 3L) == 0L) unpark();
tickCount += 1;
}
}

View File

@@ -0,0 +1,37 @@
package org.dreeam.leaf.async.ai;
import net.minecraft.Util;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import java.util.concurrent.locks.LockSupport;
public class AsyncGoalThread extends Thread {
public AsyncGoalThread(final MinecraftServer server) {
super(() -> run(server), "Leaf Async Goal Thread");
this.setDaemon(true);
this.setUncaughtExceptionHandler(Util::onThreadException);
this.setPriority(Thread.NORM_PRIORITY - 1);
this.start();
}
private static void run(MinecraftServer server) {
while (server.isRunning()) {
LockSupport.park();
for (ServerLevel level : server.getAllLevels()) {
var exec = level.asyncGoalExecutor;
while (true) {
int id = exec.queue.recv();
if (id == Integer.MAX_VALUE) {
break;
}
if (exec.wake(id)) {
while (!exec.wake.send(id)) {
Thread.onSpinWait();
}
}
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.dreeam.leaf.async.ai;
import org.jetbrains.annotations.Nullable;
public class Waker {
@Nullable
public volatile Runnable wake = null;
@Nullable
public volatile Object result = null;
public volatile boolean state = true;
public final @Nullable Object result() {
Object result = this.result;
this.result = null;
return result;
}
}

View File

@@ -1,16 +1,10 @@
package org.dreeam.leaf.config.modules.async;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.dreeam.leaf.async.ai.AsyncGoalExecutor;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsyncTargetFinding extends ConfigModules {
public String getBasePath() {
@@ -20,18 +14,17 @@ public class AsyncTargetFinding extends ConfigModules {
@Experimental
public static boolean enabled = false;
public static boolean alertOther = true;
public static boolean searchBlock = false;
public static boolean searchBlock = true;
public static boolean searchEntity = true;
public static boolean searchPlayer = false;
public static boolean searchPlayerTempt = false;
public static int queueSize = 4096;
private static boolean asyncTargetFindingInitialized;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
**Experimental feature**
This moves the expensive entity target search calculations to a background thread while
keeping the actual entity validation on the main thread.""",
This moves the expensive entity and block search calculations to background thread while
keeping the actual validation on the main thread.""",
"""
**实验性功能**
这会将昂贵的实体目标搜索计算移至后台线程, 同时在主线程上保持实际的实体验证.""");
@@ -44,29 +37,16 @@ public class AsyncTargetFinding extends ConfigModules {
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
alertOther = config.getBoolean(getBasePath() + ".async-alert-other", true);
searchBlock = config.getBoolean(getBasePath() + ".async-search-block", false);
searchBlock = config.getBoolean(getBasePath() + ".async-search-block", true);
searchEntity = config.getBoolean(getBasePath() + ".async-search-entity", true);
searchPlayer = config.getBoolean(getBasePath() + ".async-search-player", false);
searchPlayerTempt = config.getBoolean(getBasePath() + ".async-search-player-tempt", false);
queueSize = config.getInt(getBasePath() + ".queue-size", 100_000);
if (queueSize <= 0) {
queueSize = 4096;
}
if (!enabled) {
alertOther = false;
searchEntity = false;
searchBlock = false;
searchPlayer = false;
searchPlayerTempt = false;
return;
}
AsyncGoalExecutor.EXECUTOR = new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(128),
new ThreadFactoryBuilder()
.setNameFormat("Leaf Async Target Finding Thread")
.setDaemon(true)
.setPriority(Thread.NORM_PRIORITY - 2)
.build(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}

View File

@@ -0,0 +1,63 @@
package org.dreeam.leaf.util.queue;
/// Lock-free Single Producer Single Consumer Queue
public class SpscIntQueue {
private final int[] data;
private final PaddedAtomicInteger producerIdx = new PaddedAtomicInteger();
private final PaddedAtomicInteger producerCachedIdx = new PaddedAtomicInteger();
private final PaddedAtomicInteger consumerIdx = new PaddedAtomicInteger();
private final PaddedAtomicInteger consumerCachedIdx = new PaddedAtomicInteger();
public SpscIntQueue(int size) {
this.data = new int[size + 1];
}
public final boolean send(int e) {
final int idx = producerIdx.getOpaque();
int nextIdx = idx + 1;
if (nextIdx == data.length) {
nextIdx = 0;
}
int cachedIdx = consumerCachedIdx.getPlain();
if (nextIdx == cachedIdx) {
cachedIdx = consumerIdx.getAcquire();
consumerCachedIdx.setPlain(cachedIdx);
if (nextIdx == cachedIdx) {
return false;
}
}
data[idx] = e;
producerIdx.setRelease(nextIdx);
return true;
}
public final int recv() {
final int idx = consumerIdx.getOpaque();
int cachedIdx = producerCachedIdx.getPlain();
if (idx == cachedIdx) {
cachedIdx = producerIdx.getAcquire();
producerCachedIdx.setPlain(cachedIdx);
if (idx == cachedIdx) {
return Integer.MAX_VALUE;
}
}
int e = data[idx];
int nextIdx = idx + 1;
if (nextIdx == data.length) {
nextIdx = 0;
}
consumerIdx.setRelease(nextIdx);
return e;
}
public final int size() {
return this.data.length;
}
static class PaddedAtomicInteger extends java.util.concurrent.atomic.AtomicInteger {
@SuppressWarnings("unused")
private int i1, i2, i3, i4, i5, i6, i7, i8,
i9, i10, i11, i12, i13, i14, i15;
}
}