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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user