From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf
Date: Sun, 2 Oct 2022 21:28:53 -0700
Subject: [PATCH] Threaded Regions
Connection thread-safety fixes
- send packet
- pending addition
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
index f4415f782b32fed25da98e44b172f717c4d46e34..ba7c24b3627a1827721d2462add15fdd4adbed90 100644
--- a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
@@ -392,6 +392,24 @@ public class MultiThreadedQueue implements Queue {
}
}
+ /**
+ * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}.
+ */
+ public boolean isAddBlocked() {
+ for (LinkedNode tail = this.getTailOpaque();;) {
+ LinkedNode next = tail.getNextVolatile();
+ if (next == null) {
+ return false;
+ }
+
+ if (next == tail) {
+ return true;
+ }
+
+ tail = next;
+ }
+ }
+
/**
* Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
* head is removed.
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a155b779914828a0d4199bdfcb0d6fca25e1581
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java
@@ -0,0 +1,146 @@
+package ca.spottedleaf.concurrentutil.lock;
+
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.locks.LockSupport;
+
+public final class AreaLock {
+
+ private final int coordinateShift;
+
+ private final Long2ReferenceOpenHashMap nodesByPosition = new Long2ReferenceOpenHashMap<>(1024, 0.10f);
+
+ public AreaLock(final int coordinateShift) {
+ this.coordinateShift = coordinateShift;
+ }
+
+ private static long key(final int x, final int z) {
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public Node lock(final int x, final int z, final int radius) {
+ final Thread thread = Thread.currentThread();
+ final int minX = (x - radius) >> this.coordinateShift;
+ final int minZ = (z - radius) >> this.coordinateShift;
+ final int maxX = (x + radius) >> this.coordinateShift;
+ final int maxZ = (z + radius) >> this.coordinateShift;
+
+ final Node node = new Node(x, z, radius, thread);
+
+ synchronized (this) {
+ ReferenceOpenHashSet parents = null;
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final Node dependency = this.nodesByPosition.put(key(currX, currZ), node);
+ if (dependency == null) {
+ continue;
+ }
+
+ if (parents == null) {
+ parents = new ReferenceOpenHashSet<>();
+ }
+
+ if (parents.add(dependency)) {
+ // added a dependency, so we need to add as a child to the dependency
+ if (dependency.children == null) {
+ dependency.children = new ArrayList<>();
+ }
+ dependency.children.add(node);
+ }
+ }
+ }
+
+ if (parents == null) {
+ // no dependencies, so we can just return immediately
+ return node;
+ } // else: we need to lock
+
+ node.parents = parents;
+ }
+
+ while (!node.unlocked) {
+ LockSupport.park(node);
+ }
+
+ return node;
+ }
+
+ public void unlock(final Node node) {
+ List toUnpark = null;
+
+ final int x = node.x;
+ final int z = node.z;
+ final int radius = node.radius;
+
+ final int minX = (x - radius) >> this.coordinateShift;
+ final int minZ = (z - radius) >> this.coordinateShift;
+ final int maxX = (x + radius) >> this.coordinateShift;
+ final int maxZ = (z + radius) >> this.coordinateShift;
+
+ synchronized (this) {
+ final List children = node.children;
+ if (children != null) {
+ // try to unlock children
+ for (int i = 0, len = children.size(); i < len; ++i) {
+ final Node child = children.get(i);
+ if (!child.parents.remove(node)) {
+ throw new IllegalStateException();
+ }
+ if (child.parents.isEmpty()) {
+ // we can unlock, as it now has no dependencies in front
+ child.parents = null;
+ if (toUnpark == null) {
+ toUnpark = new ArrayList<>();
+ toUnpark.add(child);
+ } else {
+ toUnpark.add(child);
+ }
+ }
+ }
+ }
+
+ // remove node from dependency map
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ // node: we only remove if we match, as a mismatch indicates a child node which of course has not
+ // yet been unlocked
+ this.nodesByPosition.remove(key(currX, currZ), node);
+ }
+ }
+ }
+
+ if (toUnpark == null) {
+ return;
+ }
+
+ // we move the unpark / unlock logic here because we want to avoid performing work while holding the lock
+
+ for (int i = 0, len = toUnpark.size(); i < len; ++i) {
+ final Node toUnlock = toUnpark.get(i);
+ toUnlock.unlocked = true; // must be volatile and before unpark()
+ LockSupport.unpark(toUnlock.thread);
+ }
+ }
+
+ public static final class Node {
+
+ public final int x;
+ public final int z;
+ public final int radius;
+ public final Thread thread;
+
+ private List children;
+ private ReferenceOpenHashSet parents;
+
+ private volatile boolean unlocked;
+
+ public Node(final int x, final int z, final int radius, final Thread thread) {
+ this.x = x;
+ this.z = z;
+ this.radius = radius;
+ this.thread = thread;
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java
new file mode 100644
index 0000000000000000000000000000000000000000..7869cc177c95e26dd9e1d3db5b50e996956edb24
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java
@@ -0,0 +1,664 @@
+package ca.spottedleaf.concurrentutil.map;
+
+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+import io.papermc.paper.util.IntegerUtil;
+import java.lang.invoke.VarHandle;
+import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public class SWMRInt2IntHashTable {
+
+ protected int size;
+
+ protected TableEntry[] table;
+
+ protected final float loadFactor;
+
+ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "size", int.class);
+ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "table", TableEntry[].class);
+
+ /* size */
+
+ protected final int getSizePlain() {
+ return (int)SIZE_HANDLE.get(this);
+ }
+
+ protected final int getSizeOpaque() {
+ return (int)SIZE_HANDLE.getOpaque(this);
+ }
+
+ protected final int getSizeAcquire() {
+ return (int)SIZE_HANDLE.getAcquire(this);
+ }
+
+ protected final void setSizePlain(final int value) {
+ SIZE_HANDLE.set(this, value);
+ }
+
+ protected final void setSizeOpaque(final int value) {
+ SIZE_HANDLE.setOpaque(this, value);
+ }
+
+ protected final void setSizeRelease(final int value) {
+ SIZE_HANDLE.setRelease(this, value);
+ }
+
+ /* table */
+
+ protected final TableEntry[] getTablePlain() {
+ //noinspection unchecked
+ return (TableEntry[])TABLE_HANDLE.get(this);
+ }
+
+ protected final TableEntry[] getTableAcquire() {
+ //noinspection unchecked
+ return (TableEntry[])TABLE_HANDLE.getAcquire(this);
+ }
+
+ protected final void setTablePlain(final TableEntry[] table) {
+ TABLE_HANDLE.set(this, table);
+ }
+
+ protected final void setTableRelease(final TableEntry[] table) {
+ TABLE_HANDLE.setRelease(this, table);
+ }
+
+ protected static final int DEFAULT_CAPACITY = 16;
+ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
+
+ /**
+ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
+ */
+ public SWMRInt2IntHashTable() {
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
+ }
+
+ /**
+ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
+ * @param capacity specified initial capacity, > 0
+ */
+ public SWMRInt2IntHashTable(final int capacity) {
+ this(capacity, DEFAULT_LOAD_FACTOR);
+ }
+
+ /**
+ * Constructs this map with the specified capacity and load factor.
+ * @param capacity specified capacity, > 0
+ * @param loadFactor specified load factor, > 0 && finite
+ */
+ public SWMRInt2IntHashTable(final int capacity, final float loadFactor) {
+ final int tableSize = getCapacityFor(capacity);
+
+ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
+ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
+ }
+
+ //noinspection unchecked
+ final TableEntry[] table = new TableEntry[tableSize];
+ this.setTablePlain(table);
+
+ if (tableSize == MAXIMUM_CAPACITY) {
+ this.threshold = -1;
+ } else {
+ this.threshold = getTargetCapacity(tableSize, loadFactor);
+ }
+
+ this.loadFactor = loadFactor;
+ }
+
+ /**
+ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
+ * with a load factor of {@code 0.75f}.
+ * All of the specified map's entries are copied into this map.
+ * @param other The specified map.
+ */
+ public SWMRInt2IntHashTable(final SWMRInt2IntHashTable other) {
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
+ }
+
+ /**
+ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
+ * with a load factor of {@code 0.75f}.
+ * All of the specified map's entries are copied into this map.
+ * @param capacity specified capacity, > 0
+ * @param other The specified map.
+ */
+ public SWMRInt2IntHashTable(final int capacity, final SWMRInt2IntHashTable other) {
+ this(capacity, DEFAULT_LOAD_FACTOR, other);
+ }
+
+ /**
+ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
+ * with the specified load factor.
+ * All of the specified map's entries are copied into this map.
+ * @param capacity specified capacity, > 0
+ * @param loadFactor specified load factor, > 0 && finite
+ * @param other The specified map.
+ */
+ public SWMRInt2IntHashTable(final int capacity, final float loadFactor, final SWMRInt2IntHashTable other) {
+ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
+ this.putAll(other);
+ }
+
+ public final float getLoadFactor() {
+ return this.loadFactor;
+ }
+
+ protected static int getCapacityFor(final int capacity) {
+ if (capacity <= 0) {
+ throw new IllegalArgumentException("Invalid capacity: " + capacity);
+ }
+ if (capacity >= MAXIMUM_CAPACITY) {
+ return MAXIMUM_CAPACITY;
+ }
+ return IntegerUtil.roundCeilLog2(capacity);
+ }
+
+ /** Callers must still use acquire when reading the value of the entry. */
+ protected final TableEntry getEntryForOpaque(final int key) {
+ final int hash = SWMRInt2IntHashTable.getHash(key);
+ final TableEntry[] table = this.getTableAcquire();
+
+ for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
+ if (key == curr.key) {
+ return curr;
+ }
+ }
+
+ return null;
+ }
+
+ protected final TableEntry getEntryForPlain(final int key) {
+ final int hash = SWMRInt2IntHashTable.getHash(key);
+ final TableEntry[] table = this.getTablePlain();
+
+ for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
+ if (key == curr.key) {
+ return curr;
+ }
+ }
+
+ return null;
+ }
+
+ /* MT-Safe */
+
+ /** must be deterministic given a key */
+ protected static int getHash(final int key) {
+ return it.unimi.dsi.fastutil.HashCommon.mix(key);
+ }
+
+ // rets -1 if capacity*loadFactor is too large
+ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
+ final double ret = (double)capacity * (double)loadFactor;
+ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
+ return -1;
+ }
+
+ return (int)ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ /* Make no attempt to deal with concurrent modifications */
+ if (!(obj instanceof SWMRInt2IntHashTable)) {
+ return false;
+ }
+ final SWMRInt2IntHashTable other = (SWMRInt2IntHashTable)obj;
+
+ if (this.size() != other.size()) {
+ return false;
+ }
+
+ final TableEntry[] table = this.getTableAcquire();
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ final int value = curr.getValueAcquire();
+
+ final int otherValue = other.get(curr.key);
+ if (value != otherValue) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ /* Make no attempt to deal with concurrent modifications */
+ int hash = 0;
+ final TableEntry[] table = this.getTableAcquire();
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ hash += curr.hashCode();
+ }
+ }
+
+ return hash;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder(64);
+ builder.append("SingleWriterMultiReaderHashMap:{");
+
+ this.forEach((final int key, final int value) -> {
+ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
+ });
+
+ return builder.append('}').toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SWMRInt2IntHashTable clone() {
+ return new SWMRInt2IntHashTable(this.getTableAcquire().length, this.loadFactor, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void forEach(final Consumer super SWMRInt2IntHashTable.TableEntry> action) {
+ Validate.notNull(action, "Null action");
+
+ final TableEntry[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ action.accept(curr);
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public static interface BiIntIntConsumer {
+ public void accept(final int key, final int value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void forEach(final BiIntIntConsumer action) {
+ Validate.notNull(action, "Null action");
+
+ final TableEntry[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ final int value = curr.getValueAcquire();
+
+ action.accept(curr.key, value);
+ }
+ }
+ }
+
+ /**
+ * Provides the specified consumer with all keys contained within this map.
+ * @param action The specified consumer.
+ */
+ public void forEachKey(final IntConsumer action) {
+ Validate.notNull(action, "Null action");
+
+ final TableEntry[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ action.accept(curr.key);
+ }
+ }
+ }
+
+ /**
+ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
+ * @param action The specified consumer.
+ */
+ public void forEachValue(final IntConsumer action) {
+ Validate.notNull(action, "Null action");
+
+ final TableEntry[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+ final int value = curr.getValueAcquire();
+
+ action.accept(value);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int get(final int key) {
+ final TableEntry entry = this.getEntryForOpaque(key);
+ return entry == null ? 0 : entry.getValueAcquire();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean containsKey(final int key) {
+ final TableEntry entry = this.getEntryForOpaque(key);
+ return entry != null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int getOrDefault(final int key, final int defaultValue) {
+ final TableEntry entry = this.getEntryForOpaque(key);
+
+ return entry == null ? defaultValue : entry.getValueAcquire();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int size() {
+ return this.getSizeAcquire();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isEmpty() {
+ return this.getSizeAcquire() == 0;
+ }
+
+ /* Non-MT-Safe */
+
+ protected int threshold;
+
+ protected final void checkResize(final int minCapacity) {
+ if (minCapacity <= this.threshold || this.threshold < 0) {
+ return;
+ }
+
+ final TableEntry[] table = this.getTablePlain();
+ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
+ if (newCapacity < 0) {
+ newCapacity = MAXIMUM_CAPACITY;
+ }
+ if (newCapacity <= table.length) {
+ if (newCapacity == MAXIMUM_CAPACITY) {
+ return;
+ }
+ newCapacity = table.length << 1;
+ }
+
+ //noinspection unchecked
+ final TableEntry[] newTable = new TableEntry[newCapacity];
+ final int indexMask = newCapacity - 1;
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) {
+ final int key = entry.key;
+ final int hash = SWMRInt2IntHashTable.getHash(key);
+ final int index = hash & indexMask;
+
+ /* we need to create a new entry since there could be reading threads */
+ final TableEntry insert = new TableEntry(key, entry.getValuePlain());
+
+ final TableEntry prev = newTable[index];
+
+ newTable[index] = insert;
+ insert.setNextPlain(prev);
+ }
+ }
+
+ if (newCapacity == MAXIMUM_CAPACITY) {
+ this.threshold = -1; /* No more resizing */
+ } else {
+ this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
+ }
+ this.setTableRelease(newTable); /* use release to publish entries in table */
+ }
+
+ protected final int addToSize(final int num) {
+ final int newSize = this.getSizePlain() + num;
+
+ this.setSizeOpaque(newSize);
+ this.checkResize(newSize);
+
+ return newSize;
+ }
+
+ protected final int removeFromSize(final int num) {
+ final int newSize = this.getSizePlain() - num;
+
+ this.setSizeOpaque(newSize);
+
+ return newSize;
+ }
+
+ protected final int put(final int key, final int value, final boolean onlyIfAbsent) {
+ final TableEntry[] table = this.getTablePlain();
+ final int hash = SWMRInt2IntHashTable.getHash(key);
+ final int index = hash & (table.length - 1);
+
+ final TableEntry head = table[index];
+ if (head == null) {
+ final TableEntry insert = new TableEntry(key, value);
+ ArrayUtil.setRelease(table, index, insert);
+ this.addToSize(1);
+ return 0;
+ }
+
+ for (TableEntry curr = head;;) {
+ if (key == curr.key) {
+ if (onlyIfAbsent) {
+ return curr.getValuePlain();
+ }
+
+ final int currVal = curr.getValuePlain();
+ curr.setValueRelease(value);
+ return currVal;
+ }
+
+ final TableEntry next = curr.getNextPlain();
+ if (next != null) {
+ curr = next;
+ continue;
+ }
+
+ final TableEntry insert = new TableEntry(key, value);
+
+ curr.setNextRelease(insert);
+ this.addToSize(1);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int put(final int key, final int value) {
+ return this.put(key, value, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int putIfAbsent(final int key, final int value) {
+ return this.put(key, value, true);
+ }
+
+ protected final int remove(final int key, final int hash) {
+ final TableEntry[] table = this.getTablePlain();
+ final int index = (table.length - 1) & hash;
+
+ final TableEntry head = table[index];
+ if (head == null) {
+ return 0;
+ }
+
+ if (head.key == key) {
+ ArrayUtil.setRelease(table, index, head.getNextPlain());
+ this.removeFromSize(1);
+
+ return head.getValuePlain();
+ }
+
+ for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+ if (key == curr.key) {
+ prev.setNextRelease(curr.getNextPlain());
+ this.removeFromSize(1);
+
+ return curr.getValuePlain();
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int remove(final int key) {
+ return this.remove(key, SWMRInt2IntHashTable.getHash(key));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void putAll(final SWMRInt2IntHashTable map) {
+ Validate.notNull(map, "Null map");
+
+ final int size = map.size();
+ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
+ map.forEach(this::put);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
+ * is release ordered, that is, after the clear operation is performed a release fence is performed.
+ *
+ */
+ public void clear() {
+ Arrays.fill(this.getTablePlain(), null);
+ this.setSizeRelease(0);
+ }
+
+ public static final class TableEntry {
+
+ protected final int key;
+ protected int value;
+
+ protected TableEntry next;
+
+ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+
+ /* value */
+
+ protected final int getValuePlain() {
+ //noinspection unchecked
+ return (int)VALUE_HANDLE.get(this);
+ }
+
+ protected final int getValueAcquire() {
+ //noinspection unchecked
+ return (int)VALUE_HANDLE.getAcquire(this);
+ }
+
+ protected final void setValueRelease(final int to) {
+ VALUE_HANDLE.setRelease(this, to);
+ }
+
+ /* next */
+
+ protected final TableEntry getNextPlain() {
+ //noinspection unchecked
+ return (TableEntry)NEXT_HANDLE.get(this);
+ }
+
+ protected final TableEntry getNextOpaque() {
+ //noinspection unchecked
+ return (TableEntry)NEXT_HANDLE.getOpaque(this);
+ }
+
+ protected final void setNextPlain(final TableEntry next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ protected final void setNextRelease(final TableEntry next) {
+ NEXT_HANDLE.setRelease(this, next);
+ }
+
+ protected TableEntry(final int key, final int value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public int getKey() {
+ return this.key;
+ }
+
+ public int getValue() {
+ return this.getValueAcquire();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int setValue(final int value) {
+ final int curr = this.getValuePlain();
+
+ this.setValueRelease(value);
+ return curr;
+ }
+
+ protected static int hash(final int key, final int value) {
+ return SWMRInt2IntHashTable.getHash(key) ^ SWMRInt2IntHashTable.getHash(value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return hash(this.key, this.getValueAcquire());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (!(obj instanceof TableEntry)) {
+ return false;
+ }
+ final TableEntry other = (TableEntry)obj;
+ final int otherKey = other.getKey();
+ final int thisKey = this.getKey();
+ final int otherValue = other.getValueAcquire();
+ final int thisVal = this.getValueAcquire();
+ return (thisKey == otherKey) && (thisVal == otherValue);
+ }
+ }
+
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
index 1e98f778ffa0a7bb00ebccaaa8bde075183e41f0..aebe82cbe8bc20e5f4260a871d7b620e5092b2c9 100644
--- a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
@@ -534,6 +534,44 @@ public class SWMRLong2ObjectHashTable {
return null;
}
+ protected final V remove(final long key, final int hash, final V expect) {
+ final TableEntry[] table = this.getTablePlain();
+ final int index = (table.length - 1) & hash;
+
+ final TableEntry head = table[index];
+ if (head == null) {
+ return null;
+ }
+
+ if (head.key == key) {
+ final V val = head.value;
+ if (val == expect || val.equals(expect)) {
+ ArrayUtil.setRelease(table, index, head.getNextPlain());
+ this.removeFromSize(1);
+
+ return head.getValuePlain();
+ } else {
+ return null;
+ }
+ }
+
+ for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+ if (key == curr.key) {
+ final V val = curr.value;
+ if (val == expect || val.equals(expect)) {
+ prev.setNextRelease(curr.getNextPlain());
+ this.removeFromSize(1);
+
+ return curr.getValuePlain();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
/**
* {@inheritDoc}
*/
@@ -541,6 +579,10 @@ public class SWMRLong2ObjectHashTable {
return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
}
+ public boolean remove(final long key, final V expect) {
+ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null;
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..f579ad58ea7db20d6d7b89abbab3a4dfadaaeaee
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
@@ -0,0 +1,534 @@
+package ca.spottedleaf.concurrentutil.scheduler;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.set.LinkedSortedSet;
+import org.slf4j.Logger;
+import java.lang.invoke.VarHandle;
+import java.util.BitSet;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.BooleanSupplier;
+
+public class SchedulerThreadPool {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
+
+ private static final Comparator TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> {
+ final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart);
+ if (timeCompare != 0) {
+ return timeCompare;
+ }
+
+ return Long.compare(t1.id, t2.id);
+ };
+
+ private final TickThreadRunner[] runners;
+ private final Thread[] threads;
+ private final LinkedSortedSet awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
+ private final PriorityQueue queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME);
+ private final BitSet idleThreads;
+
+ private final Object scheduleLock = new Object();
+
+ private volatile boolean halted;
+
+ public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) {
+ final BitSet idleThreads = new BitSet(threads);
+ for (int i = 0; i < threads; ++i) {
+ idleThreads.set(i);
+ }
+ this.idleThreads = idleThreads;
+
+ final TickThreadRunner[] runners = new TickThreadRunner[threads];
+ final Thread[] t = new Thread[threads];
+ for (int i = 0; i < threads; ++i) {
+ runners[i] = new TickThreadRunner(i, this);
+ t[i] = threadFactory.newThread(runners[i]);
+ }
+
+ this.threads = t;
+ this.runners = runners;
+ }
+
+ /**
+ * Starts the threads in this pool.
+ */
+ public void start() {
+ for (final Thread thread : this.threads) {
+ thread.start();
+ }
+ }
+
+ /**
+ * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die.
+ *
+ * @param sync Whether to wait for the scheduler threads to die.
+ * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die.
+ * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout.
+ * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time.
+ */
+ public boolean halt(final boolean sync, final long maxWaitNS) {
+ this.halted = true;
+ for (final Thread thread : this.threads) {
+ // force response to halt
+ LockSupport.unpark(thread);
+ }
+ final long time = System.nanoTime();
+ if (sync) {
+ // start at 10 * 0.5ms -> 5ms
+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
+ boolean allDead = true;
+ for (final Thread thread : this.threads) {
+ if (thread.isAlive()) {
+ allDead = false;
+ break;
+ }
+ }
+ if (allDead) {
+ return true;
+ }
+ if ((System.nanoTime() - time) >= maxWaitNS) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an array of the underlying scheduling threads.
+ */
+ public Thread[] getThreads() {
+ return this.threads.clone();
+ }
+
+ private void insertFresh(final SchedulableTick task) {
+ final TickThreadRunner[] runners = this.runners;
+
+ final int firstIdleThread = this.idleThreads.nextSetBit(0);
+
+ if (firstIdleThread != -1) {
+ // push to idle thread
+ this.idleThreads.clear(firstIdleThread);
+ final TickThreadRunner runner = runners[firstIdleThread];
+ task.awaitingLink = this.awaiting.addLast(task);
+ runner.acceptTask(task);
+ return;
+ }
+
+ // try to replace the last awaiting task
+ final SchedulableTick last = this.awaiting.last();
+
+ if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) {
+ // need to replace the last task
+ this.awaiting.pollLast();
+ last.awaitingLink = null;
+ task.awaitingLink = this.awaiting.addLast(task);
+ // need to add task to queue to be picked up later
+ this.queued.add(last);
+
+ final TickThreadRunner runner = last.ownedBy;
+ runner.replaceTask(task);
+
+ return;
+ }
+
+ // add to queue, will be picked up later
+ this.queued.add(task);
+ }
+
+ private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) {
+ if (!this.awaiting.remove(tick.awaitingLink)) {
+ throw new IllegalStateException("Task is not in awaiting");
+ }
+ tick.awaitingLink = null;
+ }
+
+ private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) {
+ if (reschedule != null) {
+ this.queued.add(reschedule);
+ }
+ final SchedulableTick ret = this.queued.poll();
+ if (ret == null) {
+ this.idleThreads.set(runner.id);
+ } else {
+ ret.awaitingLink = this.awaiting.addLast(ret);
+ }
+
+ return ret;
+ }
+
+ public void schedule(final SchedulableTick task) {
+ synchronized (this.scheduleLock) {
+ if (!task.tryMarkScheduled()) {
+ throw new IllegalStateException("Task " + task + " is already scheduled or cancelled");
+ }
+
+ task.schedulerOwnedBy = this;
+
+ this.insertFresh(task);
+ }
+ }
+
+ public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) {
+ synchronized (this.scheduleLock) {
+ if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) {
+ return false;
+ }
+ if (this.queued.remove(task)) {
+ task.setScheduledStart(newStart);
+ this.queued.add(task);
+ return true;
+ }
+ if (task.awaitingLink != null) {
+ this.awaiting.remove(task.awaitingLink);
+ task.awaitingLink = null;
+
+ // re-queue task
+ task.setScheduledStart(newStart);
+ this.queued.add(task);
+
+ // now we need to replace the task the runner was waiting for
+ final TickThreadRunner runner = task.ownedBy;
+ final SchedulableTick replace = this.queued.poll();
+
+ // replace cannot be null, since we have added a task to queued
+ if (replace != task) {
+ runner.replaceTask(replace);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled
+ * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing.
+ */
+ public Boolean tryRetire(final SchedulableTick task) {
+ if (task.schedulerOwnedBy != this) {
+ return null;
+ }
+
+ synchronized (this.scheduleLock) {
+ if (this.queued.remove(task)) {
+ // cancelled, and no runner owns it - so return
+ return Boolean.TRUE;
+ }
+ if (task.awaitingLink != null) {
+ this.awaiting.remove(task.awaitingLink);
+ task.awaitingLink = null;
+ // here we need to replace the task the runner was waiting for
+ final TickThreadRunner runner = task.ownedBy;
+ final SchedulableTick replace = this.queued.poll();
+
+ if (replace == null) {
+ // nothing to replace with, set to idle
+ this.idleThreads.set(runner.id);
+ runner.forceIdle();
+ } else {
+ runner.replaceTask(replace);
+ }
+
+ return Boolean.TRUE;
+ }
+
+ // could not find it in queue
+ return task.tryMarkCancelled() ? Boolean.FALSE : null;
+ }
+ }
+
+ public void notifyTasks(final SchedulableTick task) {
+ // Not implemented
+ }
+
+ /**
+ * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}.
+ *
+ * A tickable task is expected to run on a fixed interval, which is determined by
+ * the {@link SchedulerThreadPool}.
+ *
+ *
+ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of
+ * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain
+ * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take
+ * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking.
+ *
+ *
+ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never
+ * invoked in parallel.
+ * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)}
+ * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to
+ * parse intermediate tasks.
+ *
+ */
+ public static abstract class SchedulableTick {
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
+ public final long id = ID_GENERATOR.getAndIncrement();
+
+ private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0;
+ private static final int SCHEDULE_STATE_SCHEDULED = 1;
+ private static final int SCHEDULE_STATE_CANCELLED = 2;
+
+ private final AtomicInteger scheduled = new AtomicInteger();
+ private SchedulerThreadPool schedulerOwnedBy;
+ private long scheduledStart = DEADLINE_NOT_SET;
+ private TickThreadRunner ownedBy;
+
+ private LinkedSortedSet.Link awaitingLink;
+
+ private boolean tryMarkScheduled() {
+ return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED);
+ }
+
+ private boolean tryMarkCancelled() {
+ return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED);
+ }
+
+ private boolean isScheduled() {
+ return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED;
+ }
+
+ protected final long getScheduledStart() {
+ return this.scheduledStart;
+ }
+
+ /**
+ * If this task is scheduled, then this may only be invoked during {@link #runTick()},
+ * and {@link #runTasks(BooleanSupplier)}
+ */
+ protected final void setScheduledStart(final long value) {
+ this.scheduledStart = value;
+ }
+
+ /**
+ * Executes the tick.
+ *
+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
+ * the next tick.
+ *
+ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise.
+ */
+ public abstract boolean runTick();
+
+ /**
+ * Returns whether this task has any intermediate tasks that can be executed.
+ */
+ public abstract boolean hasTasks();
+
+ /**
+ * Returns {@code null} if this task should not be scheduled, otherwise returns
+ * {@code Boolean.TRUE} if there are more intermediate tasks to execute and
+ * {@code Boolean.FALSE} if there are no more intermediate tasks to execute.
+ */
+ public abstract Boolean runTasks(final BooleanSupplier canContinue);
+
+ @Override
+ public String toString() {
+ return "SchedulableTick:{" +
+ "class=" + this.getClass().getName() + "," +
+ "scheduled_state=" + this.scheduled.get() + ","
+ + "}";
+ }
+ }
+
+ private static final class TickThreadRunner implements Runnable {
+
+ /**
+ * There are no tasks in this thread's runqueue, so it is parked.
+ *
+ * stateTarget = null
+ *
+ */
+ private static final int STATE_IDLE = 0;
+
+ /**
+ * The runner is waiting to tick a task, as it has no intermediate tasks to execute.
+ *
+ * stateTarget = the task awaiting tick
+ *
+ */
+ private static final int STATE_AWAITING_TICK = 1;
+
+ /**
+ * The runner is executing a tick for one of the tasks that was in its runqueue.
+ *
+ * stateTarget = the task being ticked
+ *
+ */
+ private static final int STATE_EXECUTING_TICK = 2;
+
+ public final int id;
+ public final SchedulerThreadPool scheduler;
+
+ private volatile Thread thread;
+ private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE);
+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class);
+
+ private void setStatePlain(final TickThreadRunnerState state) {
+ STATE_HANDLE.set(this, state);
+ }
+
+ private void setStateOpaque(final TickThreadRunnerState state) {
+ STATE_HANDLE.setOpaque(this, state);
+ }
+
+ private void setStateVolatile(final TickThreadRunnerState state) {
+ STATE_HANDLE.setVolatile(this, state);
+ }
+
+ private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {}
+
+ public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) {
+ this.id = id;
+ this.scheduler = scheduler;
+ }
+
+ private Thread getRunnerThread() {
+ return this.thread;
+ }
+
+ private void acceptTask(final SchedulableTick task) {
+ if (task.ownedBy != null) {
+ throw new IllegalStateException("Already owned by another runner");
+ }
+ task.ownedBy = this;
+ final TickThreadRunnerState state = this.state;
+ if (state.state != STATE_IDLE) {
+ throw new IllegalStateException("Cannot accept task in state " + state);
+ }
+ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
+ LockSupport.unpark(this.getRunnerThread());
+ }
+
+ private void replaceTask(final SchedulableTick task) {
+ final TickThreadRunnerState state = this.state;
+ if (state.state != STATE_AWAITING_TICK) {
+ throw new IllegalStateException("Cannot replace task in state " + state);
+ }
+ if (task.ownedBy != null) {
+ throw new IllegalStateException("Already owned by another runner");
+ }
+ task.ownedBy = this;
+
+ state.stateTarget.ownedBy = null;
+
+ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
+ LockSupport.unpark(this.getRunnerThread());
+ }
+
+ private void forceIdle() {
+ final TickThreadRunnerState state = this.state;
+ if (state.state != STATE_AWAITING_TICK) {
+ throw new IllegalStateException("Cannot replace task in state " + state);
+ }
+ state.stateTarget.ownedBy = null;
+ this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE));
+ // no need to unpark
+ }
+
+ private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) {
+ synchronized (this.scheduler.scheduleLock) {
+ if (this.state != state) {
+ return false;
+ }
+ this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK));
+ this.scheduler.takeTask(this, task);
+ return true;
+ }
+ }
+
+ private void returnTask(final SchedulableTick task, final boolean reschedule) {
+ synchronized (this.scheduler.scheduleLock) {
+ task.ownedBy = null;
+
+ final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null);
+ if (newWait == null) {
+ this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE));
+ } else {
+ if (newWait.ownedBy != null) {
+ throw new IllegalStateException("Already owned by another runner");
+ }
+ newWait.ownedBy = this;
+ this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK));
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ this.thread = Thread.currentThread();
+
+ main_state_loop:
+ for (;;) {
+ final TickThreadRunnerState startState = this.state;
+ final int startStateType = startState.state;
+ final SchedulableTick startStateTask = startState.stateTarget;
+
+ if (this.scheduler.halted) {
+ return;
+ }
+
+ switch (startStateType) {
+ case STATE_IDLE: {
+ while (this.state.state == STATE_IDLE) {
+ LockSupport.park();
+ if (this.scheduler.halted) {
+ return;
+ }
+ }
+ continue main_state_loop;
+ }
+
+ case STATE_AWAITING_TICK: {
+ final long deadline = startStateTask.getScheduledStart();
+ for (;;) {
+ if (this.state != startState) {
+ continue main_state_loop;
+ }
+ final long diff = deadline - System.nanoTime();
+ if (diff <= 0L) {
+ break;
+ }
+ LockSupport.parkNanos(startState, diff);
+ if (this.scheduler.halted) {
+ return;
+ }
+ }
+
+ if (!this.takeTask(startState, startStateTask)) {
+ continue main_state_loop;
+ }
+
+ // TODO exception handling
+ final boolean reschedule = startStateTask.runTick();
+
+ this.returnTask(startStateTask, reschedule);
+
+ continue main_state_loop;
+ }
+
+ case STATE_EXECUTING_TICK: {
+ throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread");
+ }
+
+ default: {
+ throw new IllegalStateException("Unknown state: " + startState);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
@@ -0,0 +1,60 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class TimeUtil {
+
+ /*
+ * The comparator is not a valid comparator for every long value. To prove where it is valid, see below.
+ *
+ * For reflexivity, we have that x - x = 0. We then have that for any long value x that
+ * compareTimes(x, x) == 0, as expected.
+ *
+ * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE.
+ * So, the difference between any times x and y must not be equal to Long.MIN_VALUE.
+ *
+ * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that
+ * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values,
+ * this holds as long as a + b <= Long.MAX_VALUE.
+ *
+ * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have
+ * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE.
+ *
+ * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps
+ * c and d.
+ */
+
+ /**
+ * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which
+ * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2.
+ */
+ public static int compareTimes(final long t1, final long t2) {
+ final long diff = t1 - t2;
+
+ // HD, Section 2-7
+ return (int) ((diff >> 63) | (-diff >>> 63));
+ }
+
+ public static long getGreatestTime(final long t1, final long t2) {
+ final long diff = t1 - t2;
+ return diff < 0L ? t2 : t1;
+ }
+
+ public static long getLeastTime(final long t1, final long t2) {
+ final long diff = t1 - t2;
+ return diff > 0L ? t2 : t1;
+ }
+
+ public static long clampTime(final long value, final long min, final long max) {
+ final long diffMax = value - max;
+ final long diffMin = value - min;
+
+ if (diffMax > 0L) {
+ return max;
+ }
+ if (diffMin < 0L) {
+ return min;
+ }
+ return value;
+ }
+
+ private TimeUtil() {}
+}
diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java
new file mode 100644
index 0000000000000000000000000000000000000000..14a4778f7913b849fabbd772f9cb8a0bc5a6ed6c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java
@@ -0,0 +1,58 @@
+package ca.spottedleaf.leafprofiler;
+
+import ca.spottedleaf.concurrentutil.map.SWMRInt2IntHashTable;
+import java.util.Arrays;
+
+public final class LProfileGraph {
+
+ public static final int ROOT_NODE = 0;
+
+ // volatile required for correct publishing after resizing
+ private volatile SWMRInt2IntHashTable[] nodes = new SWMRInt2IntHashTable[16];
+ private int nodeCount;
+
+ public LProfileGraph() {
+ this.nodes[ROOT_NODE] = new SWMRInt2IntHashTable();
+ this.nodeCount = 1;
+ }
+
+ private int createNode(final int parent, final int type) {
+ synchronized (this) {
+ SWMRInt2IntHashTable[] nodes = this.nodes;
+
+ final SWMRInt2IntHashTable node = nodes[parent];
+
+ final int newNode = this.nodeCount;
+ final int prev = node.putIfAbsent(type, newNode);
+
+ if (prev != 0) {
+ // already exists
+ return prev;
+ }
+
+ // insert new node
+ ++this.nodeCount;
+
+ if (newNode >= nodes.length) {
+ this.nodes = nodes = Arrays.copyOf(nodes, nodes.length * 2);
+ }
+
+ nodes[newNode] = new SWMRInt2IntHashTable();
+
+ return newNode;
+ }
+ }
+
+ public int getOrCreateNode(final int parent, final int type) {
+ // note: requires parent node to exist
+ final SWMRInt2IntHashTable[] nodes = this.nodes;
+
+ final int mapping = nodes[parent].get(type);
+
+ if (mapping != 0) {
+ return mapping;
+ }
+
+ return this.createNode(parent, type);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java
new file mode 100644
index 0000000000000000000000000000000000000000..ffa32c1eae22bda371dd1d0318cc7c587f8e5a5c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java
@@ -0,0 +1,59 @@
+package ca.spottedleaf.leafprofiler;
+
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class LProfilerRegistry {
+
+ // volatile required to ensure correct publishing when resizing
+ private volatile ProfilerEntry[] typesById = new ProfilerEntry[16];
+ private int totalEntries;
+ private final ConcurrentHashMap nameToEntry = new ConcurrentHashMap<>();
+
+ public LProfilerRegistry() {
+
+ }
+
+ public ProfilerEntry getById(final int id) {
+ final ProfilerEntry[] entries = this.typesById;
+
+ return id < 0 || id >= entries.length ? null : entries[id];
+ }
+
+ public ProfilerEntry getByName(final String name) {
+ return this.nameToEntry.get(name);
+ }
+
+ public int createType(final ProfileType type, final String name) {
+ synchronized (this) {
+ final int id = this.totalEntries;
+
+ final ProfilerEntry ret = new ProfilerEntry(type, name, id);
+
+ final ProfilerEntry prev = this.nameToEntry.putIfAbsent(name, ret);
+
+ if (prev != null) {
+ throw new IllegalStateException("Entry already exists: " + prev);
+ }
+
+ ++this.totalEntries;
+
+ ProfilerEntry[] entries = this.typesById;
+
+ if (id >= entries.length) {
+ this.typesById = entries = Arrays.copyOf(entries, entries.length * 2);
+ }
+
+ // should be opaque, but I don't think that matters here.
+ entries[id] = ret;
+
+ return id;
+ }
+ }
+
+ public static enum ProfileType {
+ TIMER, COUNTER
+ }
+
+ public static record ProfilerEntry(ProfileType type, String name, int id) {}
+}
diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad8c590fe7479fcb3c7ff5dc3ac3a4d6f33c5938
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java
@@ -0,0 +1,61 @@
+package ca.spottedleaf.leafprofiler;
+
+import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
+import java.util.Arrays;
+
+public final class LeafProfiler {
+
+ public final LProfilerRegistry registry;
+ public final LProfileGraph graph;
+
+ private long[] data;
+ private final IntArrayFIFOQueue callStack = new IntArrayFIFOQueue();
+ private int topOfStack = LProfileGraph.ROOT_NODE;
+ private final LongArrayFIFOQueue timerStack = new LongArrayFIFOQueue();
+ private long lastTimerStart = 0L;
+
+ public LeafProfiler(final LProfilerRegistry registry, final LProfileGraph graph) {
+ this.registry = registry;
+ this.graph = graph;
+ }
+
+ private long[] resizeData(final long[] old, final int least) {
+ return this.data = Arrays.copyOf(old, Math.max(old.length * 2, least * 2));
+ }
+
+ private void incrementDirect(final int nodeId, final long count) {
+ final long[] data = this.data;
+ if (nodeId >= data.length) {
+ this.resizeData(data, nodeId)[nodeId] += count;
+ } else {
+ data[nodeId] += count;
+ }
+ }
+
+ public void incrementCounter(final int type, final long count) {
+ // this is supposed to be an optimised version of startTimer then stopTimer
+ final int node = this.graph.getOrCreateNode(this.topOfStack, type);
+ this.incrementDirect(node, count);
+ }
+
+ public void startTimer(final int type, final long startTime) {
+ final int parentNode = this.topOfStack;
+ final int newNode = this.graph.getOrCreateNode(parentNode, type);
+ this.callStack.enqueue(parentNode);
+ this.topOfStack = newNode;
+
+ this.timerStack.enqueue(this.lastTimerStart);
+ this.lastTimerStart = startTime;
+ }
+
+ public void stopTimer(final int type, final long endTime) {
+ final int currentNode = this.topOfStack;
+ this.topOfStack = this.callStack.dequeueLastInt();
+
+ final long lastStart = this.lastTimerStart;
+ this.lastTimerStart = this.timerStack.dequeueLastLong();
+
+ this.incrementDirect(currentNode, endTime - lastStart);
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
index 4b002e8b75d117b726b0de274a76d3596fce015b..897cb94abf7b53da8ba7cda5135b6580aa2d9824 100644
--- a/src/main/java/com/destroystokyo/paper/Metrics.java
+++ b/src/main/java/com/destroystokyo/paper/Metrics.java
@@ -593,7 +593,7 @@ public class Metrics {
boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
// Only start Metrics, if it's enabled in the config
if (config.getBoolean("enabled", true)) {
- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
+ Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page
metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
String minecraftVersion = Bukkit.getVersion();
@@ -611,7 +611,7 @@ public class Metrics {
} else {
paperVersion = "unknown";
}
- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion));
+ metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> paperVersion)); // Tuinity - we have our own bstats page
metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
Map> map = new HashMap<>();
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
index cab91880a08c6fdc545804911d295e0f24f4d983..8f2bff24256f0946f38e1d8b0ae9c8c921b0a2db 100644
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -179,11 +179,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
return;
}
- if (!Bukkit.isPrimaryThread()) {
- // Plugins?
- MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
- return;
- }
+ // Folia - region threading
LevelChunk chunk = chunkPacketInfo.getChunk();
int x = chunk.getPos().x;
diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
index 22a2547810d0c029f29685faddf7ac21cde2df0b..e36b4053eb2676e934b8c9c401bf58cfa7dd969c 100644
--- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
@@ -832,14 +832,14 @@ public class RedstoneWireTurbo {
j = getMaxCurrentStrength(upd, j);
int l = 0;
- wire.shouldSignal = false;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
// Unfortunately, World.isBlockIndirectlyGettingPowered is complicated,
// and I'm not ready to try to replicate even more functionality from
// elsewhere in Minecraft into this accelerator. So sadly, we must
// suffer the performance hit of this very expensive call. If there
// is consistency to what this call returns, we may be able to cache it.
final int k = worldIn.getBestNeighborSignal(upd.self);
- wire.shouldSignal = true;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
// The variable 'k' holds the maximum redstone power value of any adjacent blocks.
// If 'k' has the highest level of all neighbors, then the power level of this
diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
index 4d9bc4a62ebae0f3707900503576c64733de639f..9ba89a4bf4ae4e42418eca18f9d8a109e6c56a14 100644
--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
@@ -98,7 +98,7 @@ public final class ChatProcessor {
final CraftPlayer player = this.player.getBukkitEntity();
final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server));
this.post(ae);
- if (listenersOnSyncEvent) {
+ if (false && listenersOnSyncEvent) { // Folia - region threading
final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients());
se.setCancelled(ae.isCancelled()); // propagate cancelled state
this.queueIfAsyncOrRunImmediately(new Waitable() {
@@ -178,7 +178,7 @@ public final class ChatProcessor {
ae.setCancelled(cancelled); // propagate cancelled state
this.post(ae);
final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList());
- if (listenersOnSyncEvent) {
+ if (false && listenersOnSyncEvent) { // Folia - region threading
this.queueIfAsyncOrRunImmediately(new Waitable() {
@Override
protected Void evaluate() {
diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
index 3c17001bcd3862a76a22df488bff80a0ff4d1b83..b2fffaa862df045bacb346f3cbe7eb963e104e47 100644
--- a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
+++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
@@ -23,35 +23,42 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider {
public static final class CallbackManager {
- private final Map callbacks = new HashMap<>();
- private final Queue queue = new ConcurrentLinkedQueue<>();
+ private final java.util.concurrent.ConcurrentHashMap callbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading
+ // Folia - region threading
private CallbackManager() {
}
public UUID addCallback(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) {
final UUID id = UUID.randomUUID();
- this.queue.add(new StoredCallback(callback, options, id));
+ final StoredCallback scb = new StoredCallback(callback, options, id); // Folia - region threading
+ this.callbacks.put(scb.id(), scb); // Folia - region threading
return id;
}
public void handleQueue(final int currentTick) {
// Evict expired entries
if (currentTick % 100 == 0) {
- this.callbacks.values().removeIf(callback -> !callback.valid());
+ this.callbacks.values().removeIf(StoredCallback::expired); // Folia - region threading - don't read uses field
}
- // Add entries from queue
- StoredCallback callback;
- while ((callback = this.queue.poll()) != null) {
- this.callbacks.put(callback.id(), callback);
- }
+ // Folia - region threading
}
public void runCallback(final @NotNull Audience audience, final UUID id) {
- final StoredCallback callback = this.callbacks.get(id);
- if (callback != null && callback.valid()) { //TODO Message if expired/invalid?
- callback.takeUse();
+ // Folia start - region threading
+ final StoredCallback[] use = new StoredCallback[1];
+ this.callbacks.computeIfPresent(id, (final UUID keyInMap, final StoredCallback value) -> {
+ if (!value.valid()) {
+ return null;
+ }
+ use[0] = value;
+ value.takeUse();
+ return value.valid() ? value : null;
+ });
+ final StoredCallback callback = use[0];
+ if (callback != null) { //TODO Message if expired/invalid?
+ // Folia end - region threading
callback.callback.accept(audience);
}
}
diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
index 61d03808c8d1ab822d9b2f31fab0de14089a3b15..9051a556fea7ee35014db7bdd75b5476f672f5a9 100644
--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
@@ -91,6 +91,9 @@ public final class ChunkSystem {
for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
}
+ // Folia start - threaded regions
+ level.regioniser.addChunk(holder.pos.x, holder.pos.z);
+ // Folia end - threaded regions
}
public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
@@ -98,6 +101,9 @@ public final class ChunkSystem {
for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
}
+ // Folia start - threaded regions
+ level.regioniser.removeChunk(holder.pos.x, holder.pos.z);
+ // Folia end - threaded regions
}
public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
@@ -109,19 +115,19 @@ public final class ChunkSystem {
}
public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().tickingChunks.add(chunk);
+ // Folia - region threading
}
public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().tickingChunks.remove(chunk);
+ // Folia - region threading
}
public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().entityTickingChunks.add(chunk);
+ chunk.level.getCurrentWorldData().addEntityTickingChunks(chunk); // Folia - region threading
}
public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
+ chunk.level.getCurrentWorldData().removeEntityTickingChunk(chunk); // Folia - region threading
}
public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
index 99dc2038aa4fb0f70dc3670e451018ff541d3a85..5b4025178c1e476ed5dd0808cc33bf1ec7c08b66 100644
--- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
@@ -232,17 +232,19 @@ public class RegionizedPlayerChunkLoader {
public void tick() {
TickThread.ensureTickThread("Cannot tick player chunk loader async");
long currTime = System.nanoTime();
- for (final ServerPlayer player : this.world.players()) {
+ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threding
player.chunkLoader.update();
player.chunkLoader.midTickUpdate(currTime);
}
}
- public void tickMidTick() {
+ public boolean tickMidTick() { // Folia - region threading - report whether tickets were added
final long time = System.nanoTime();
- for (final ServerPlayer player : this.world.players()) {
- player.chunkLoader.midTickUpdate(time);
+ boolean ret = false; // Folia - region threading - report whether tickets were added
+ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threading
+ ret |= player.chunkLoader.midTickUpdate(time);
}
+ return ret; // Folia - region threading - report whether tickets were added
}
private static long[] generateBFSOrder(final int radius) {
@@ -384,13 +386,13 @@ public class RegionizedPlayerChunkLoader {
this.player = player;
}
- private void flushDelayedTicketOps() {
+ private boolean flushDelayedTicketOps() { // Folia - region threading - report whether tickets were added
if (this.delayedTicketOps.isEmpty()) {
- return;
+ return false; // Folia - region threading - report whether tickets were added
}
this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps);
this.delayedTicketOps.clear();
- this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates();
+ return this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates() == Boolean.TRUE; // Folia - region threading - report whether tickets were added
}
private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation, ?> op) {
@@ -607,7 +609,7 @@ public class RegionizedPlayerChunkLoader {
return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
}
- void midTickUpdate(final long time) {
+ boolean midTickUpdate(final long time) { // Folia - region threading - report whether tickets were added
TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
// update rate limits
this.chunkSendCounter.update(time);
@@ -799,7 +801,7 @@ public class RegionizedPlayerChunkLoader {
this.chunkSendCounter.addTime(time, sendSlots);
}
- this.flushDelayedTicketOps();
+ return this.flushDelayedTicketOps(); // Folia - region threading - report whether tickets were added
// we assume propagate ticket levels happens after this call
}
diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
index 61c170555c8854b102c640b0b6a615f9f732edbf..515cc130a411f218ed20628eb918be9d770b9939 100644
--- a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
+++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
@@ -187,7 +187,12 @@ public final class EntityLookup implements LevelEntityGetter {
@Override
public Iterable getAll() {
- return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size());
+ // Folia start - region threading
+ synchronized (this.accessibleEntities) {
+ Entity[] iterate = java.util.Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size());
+ return new ArrayIterable<>(iterate, 0, iterate.length);
+ }
+ // Folia end - region threading
}
@Override
@@ -261,7 +266,9 @@ public final class EntityLookup implements LevelEntityGetter {
if (newVisibility.ordinal() > oldVisibility.ordinal()) {
// status upgrade
if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
+ synchronized (this.accessibleEntities) { // Folia - region threading
this.accessibleEntities.add(entity);
+ } // Folia - region threading
EntityLookup.this.worldCallback.onTrackingStart(entity);
}
@@ -275,7 +282,9 @@ public final class EntityLookup implements LevelEntityGetter {
}
if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
+ synchronized (this.accessibleEntities) { // Folia - region threading
this.accessibleEntities.remove(entity);
+ } // Folia - region threading
EntityLookup.this.worldCallback.onTrackingEnd(entity);
}
}
@@ -385,11 +394,26 @@ public final class EntityLookup implements LevelEntityGetter {
entity.setLevelCallback(new EntityCallback(entity));
+ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading
+
this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
return true;
}
+ // Folia start - region threading
+ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world
+ public boolean addEntityForShutdownTeleportComplete(final Entity entity) {
+ final BlockPos pos = entity.blockPosition();
+ final int sectionX = pos.getX() >> 4;
+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
+ final int sectionZ = pos.getZ() >> 4;
+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
+
+ return slices.addEntity(entity, sectionY);
+ }
+ // Folia end - region threading
+
private void removeEntity(final Entity entity) {
final int sectionX = entity.sectionX;
final int sectionY = entity.sectionY;
@@ -407,6 +431,7 @@ public final class EntityLookup implements LevelEntityGetter {
LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
}
}
+
entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE;
this.entityByLock.writeLock();
@@ -823,6 +848,9 @@ public final class EntityLookup implements LevelEntityGetter {
EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
this.entity.setLevelCallback(NoOpCallback.INSTANCE);
+
+ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading
+ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading
}
}
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
index c6d20bc2f0eab737338db6b88dacb63f0decb66c..0d1896d09d419c78501bbccca97424dd1545230b 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
@@ -3,7 +3,6 @@ package io.papermc.paper.chunk.system.scheduling;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
-import co.aikar.timings.Timing;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -19,10 +18,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import net.minecraft.nbt.CompoundTag;
import io.papermc.paper.chunk.system.ChunkSystem;
import net.minecraft.server.MinecraftServer;
@@ -34,8 +35,6 @@ import net.minecraft.server.level.TicketType;
import net.minecraft.util.SortedArraySet;
import net.minecraft.util.Unit;
import net.minecraft.world.level.ChunkPos;
-import net.minecraft.world.level.chunk.ChunkAccess;
-import net.minecraft.world.level.chunk.ChunkStatus;
import org.bukkit.plugin.Plugin;
import org.slf4j.Logger;
import java.io.IOException;
@@ -54,6 +53,13 @@ import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
+// Folia start - region threading
+import io.papermc.paper.threadedregions.RegionizedServer;
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
+import io.papermc.paper.threadedregions.TickRegionScheduler;
+import io.papermc.paper.threadedregions.TickRegions;
+// Folia end - region threading
+
public final class ChunkHolderManager {
private static final Logger LOGGER = LogUtils.getClassLogger();
@@ -63,40 +69,198 @@ public final class ChunkHolderManager {
public static final int ENTITY_TICKING_TICKET_LEVEL = 31;
public static final int MAX_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; // inclusive
- private static final long NO_TIMEOUT_MARKER = -1L;
+ // Folia start - region threading
+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
+ // special region threading fields
+ // this field contains chunk holders that were created in addTicketAtLevel
+ // because the chunk holders were created without a reliable unload hook (i.e creation for entity/poi loading,
+ // which always check for unload after their tasks finish) we need to do that ourselves later
+ private final ReferenceOpenHashSet specialCaseUnload = new ReferenceOpenHashSet<>();
+ // Folia end - region threading
- final ReentrantLock ticketLock = new ReentrantLock();
+ public final ReentrantLock ticketLock = new ReentrantLock(); // Folia - region threading
private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f);
- private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f);
- // what a disaster of a name
- // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick
- private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>();
+ // Folia - region threading
private final ServerLevel world;
private final ChunkTaskScheduler taskScheduler;
- private long currentTick;
- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
- if (c1 == c2) {
- return 0;
+ // Folia start - region threading
+ public static final class HolderManagerRegionData {
+ /*
+ * This region data is a bit of a mess, because it is part global state and part region state.
+ * Typically for region state we do not need to worry about threading concerns because it is only
+ * accessed by the current region when ticking. But since this contains state (
+ * tickets, and removeTickToChunkExpireTicketCount) that can be written to by any thread holding the
+ * ticket lock, the merge logic is complicated as merging only holds the region lock. So, Folia has modified
+ * the add and remove ticket functions to acquire the region lock if the current region does not own the target
+ * position.
+ */
+ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
+ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+ if (c1 == c2) {
+ return 0;
+ }
+
+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+
+ if (saveTickCompare != 0) {
+ return saveTickCompare;
+ }
+
+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+
+ if (coord1 == coord2) {
+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ }
+
+ return Long.compare(coord1, coord2);
+ });
+ private long currentTick;
+ private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f);
+ // what a disaster of a name
+ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick
+ private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>();
+
+ public void merge(final HolderManagerRegionData into, final long tickOffset) {
+ // Order doesn't really matter for the pending full update...
+ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
+
+ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
+ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
+ // addition to every entry will not affect compareTo).
+ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
+ holder.lastAutoSave += tickOffset;
+ into.autoSaveQueue.add(holder);
+ }
+
+ final long chunkManagerTickOffset = into.currentTick - this.currentTick;
+ for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry>> entry = iterator.next();
+ final SortedArraySet> oldTickets = entry.getValue();
+ final SortedArraySet> newTickets = SortedArraySet.create(Math.max(4, oldTickets.size() + 1));
+ for (final Ticket> ticket : oldTickets) {
+ newTickets.add(
+ new Ticket(ticket.getType(), ticket.getTicketLevel(), ticket.key,
+ ticket.removalTick == NO_TIMEOUT_MARKER ? NO_TIMEOUT_MARKER : ticket.removalTick + chunkManagerTickOffset)
+ );
+ }
+ into.tickets.put(entry.getLongKey(), newTickets);
+ }
+ for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry entry = iterator.next();
+ into.removeTickToChunkExpireTicketCount.merge(
+ (long)(entry.getLongKey() + chunkManagerTickOffset), entry.getValue(),
+ (final Long2IntOpenHashMap t, final Long2IntOpenHashMap f) -> {
+ for (final Iterator itr = f.long2IntEntrySet().fastIterator(); itr.hasNext();) {
+ final Long2IntMap.Entry e = itr.next();
+ t.addTo(e.getLongKey(), e.getIntValue());
+ }
+ return t;
+ }
+ );
+ }
+ }
+
+ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData,
+ final ReferenceOpenHashSet dataSet) {
+ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
+ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
+ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
+
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+ if (data != null) {
+ data.pendingFullLoadUpdate.add(fullLoadUpdate);
+ } // else: fullLoadUpdate is an unloaded chunk holder
+ }
+
+ for (final NewChunkHolder autoSave : this.autoSaveQueue) {
+ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
+ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
+
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+ if (data != null) {
+ data.autoSaveQueue.add(autoSave);
+ } // else: autoSave is an unloaded chunk holder
+ }
+ for (final HolderManagerRegionData data : dataSet) {
+ data.currentTick = this.currentTick;
+ }
+ for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry>> entry = iterator.next();
+ final long chunkKey = entry.getLongKey();
+ final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift;
+ final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift;
+
+ // can never be null, since a chunk holder exists if the ticket set is not empty
+ regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)).tickets.put(chunkKey, entry.getValue());
+ }
+ for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry entry = iterator.next();
+ final long tick = entry.getLongKey();
+ final Long2IntOpenHashMap chunkToCount = entry.getValue();
+
+ for (final Iterator itr = chunkToCount.long2IntEntrySet().fastIterator(); itr.hasNext();) {
+ final Long2IntMap.Entry e = itr.next();
+ final long chunkKey = e.getLongKey();
+ final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift;
+ final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift;
+ final int count = e.getIntValue();
+
+ // can never be null, since a chunk holder exists if the ticket set is not empty
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+
+ data.removeTickToChunkExpireTicketCount.computeIfAbsent(tick, (final long keyInMap) -> {
+ return new Long2IntOpenHashMap();
+ }).put(chunkKey, count);
+ }
+ }
+ }
+ }
+
+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
+ final ThreadedRegionizer.ThreadedRegion region =
+ TickRegionScheduler.getCurrentRegion();
+
+ if (region == null) {
+ return null;
+ }
+
+ if (this.world != null && this.world != region.getData().world) {
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
}
- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+ return region.getData().getHolderManagerRegionData();
+ }
+
+ // MUST hold ticket lock
+ private ChunkHolderManager.HolderManagerRegionData getDataFor(final long key) {
+ return this.getDataFor(CoordinateUtils.getChunkX(key), CoordinateUtils.getChunkZ(key));
+ }
- if (saveTickCompare != 0) {
- return saveTickCompare;
+ // MUST hold ticket lock
+ private ChunkHolderManager.HolderManagerRegionData getDataFor(final int chunkX, final int chunkZ) {
+ if (!this.ticketLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("Must hold ticket level lock");
}
- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+ final ThreadedRegionizer.ThreadedRegion region
+ = this.world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
- if (coord1 == coord2) {
- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ if (region == null) {
+ return null;
}
- return Long.compare(coord1, coord2);
- });
+ return region.getData().getHolderManagerRegionData();
+ }
+ // Folia end - region threading
+
public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
this.world = world;
@@ -129,8 +293,13 @@ public final class ChunkHolderManager {
}
public void close(final boolean save, final boolean halt) {
+ // Folia start - region threading
+ this.close(save, halt, true, true, true);
+ }
+ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
+ // Folia end - region threading
TickThread.ensureTickThread("Closing world off-main");
- if (halt) {
+ if (first && halt) { // Folia - region threading
LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'");
if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'");
@@ -140,9 +309,10 @@ public final class ChunkHolderManager {
}
if (save) {
- this.saveAllChunks(true, true, true);
+ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
}
+ if (last) { // Folia - region threading
if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) {
RegionFileIOThread.flush();
}
@@ -163,27 +333,34 @@ public final class ChunkHolderManager {
} catch (final IOException ex) {
LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex);
}
+ } // Folia - region threading
}
void ensureInAutosave(final NewChunkHolder holder) {
- if (!this.autoSaveQueue.contains(holder)) {
- holder.lastAutoSave = MinecraftServer.currentTick;
- this.autoSaveQueue.add(holder);
+ // Folia start - region threading
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
+ if (!regionData.autoSaveQueue.contains(holder)) {
+ holder.lastAutoSave = RegionizedServer.getCurrentTick();
+ // Folia end - region threading
+ regionData.autoSaveQueue.add(holder);
}
}
public void autoSave() {
final List reschedule = new ArrayList<>();
- final long currentTick = MinecraftServer.currentTickLong;
+ final long currentTick = RegionizedServer.getCurrentTick();
final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value();
- for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) {
- final NewChunkHolder holder = this.autoSaveQueue.first();
+ // Folia start - region threading
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
+ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !regionData.autoSaveQueue.isEmpty();) {
+ // Folia end - region threading
+ final NewChunkHolder holder = regionData.autoSaveQueue.first();
if (holder.lastAutoSave > maxSaveTime) {
break;
}
- this.autoSaveQueue.remove(holder);
+ regionData.autoSaveQueue.remove(holder);
holder.lastAutoSave = currentTick;
if (holder.save(false, false) != null) {
@@ -197,15 +374,20 @@ public final class ChunkHolderManager {
for (final NewChunkHolder holder : reschedule) {
if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
- this.autoSaveQueue.add(holder);
+ regionData.autoSaveQueue.add(holder);
}
}
}
public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
+ // Folia start - region threading
+ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true);
+ }
+ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) {
+ // Folia end - region threading
final List holders = this.getChunkHolders();
- if (logProgress) {
+ if (first && logProgress) { // Folia - region threading
LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'");
}
@@ -213,7 +395,7 @@ public final class ChunkHolderManager {
int saved = 0;
- long start = System.nanoTime();
+ final long start = System.nanoTime();
long lastLog = start;
boolean needsFlush = false;
final int flushInterval = 50;
@@ -224,6 +406,12 @@ public final class ChunkHolderManager {
for (int i = 0, len = holders.size(); i < len; ++i) {
final NewChunkHolder holder = holders.get(i);
+ // Folia start - region threading
+ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
+ // skip holders that would fail the thread check
+ continue;
+ }
+ // Folia end - region threading
try {
final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false);
if (saveStat != null) {
@@ -256,7 +444,7 @@ public final class ChunkHolderManager {
}
}
}
- if (flush) {
+ if (last && flush) { // Folia - region threading
RegionFileIOThread.flush();
}
if (logProgress) {
@@ -290,18 +478,16 @@ public final class ChunkHolderManager {
}
public boolean hasTickets() {
- this.ticketLock.lock();
- try {
- return !this.tickets.isEmpty();
- } finally {
- this.ticketLock.unlock();
- }
+ return !this.getTicketsCopy().isEmpty(); // Folia - region threading
}
public String getTicketDebugString(final long coordinate) {
this.ticketLock.lock();
try {
- final SortedArraySet> tickets = this.tickets.get(coordinate);
+ // Folia start - region threading
+ final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate);
+ final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate);
+ // Folia end - region threading
return tickets != null ? tickets.first().toString() : "no_ticket";
} finally {
@@ -312,7 +498,17 @@ public final class ChunkHolderManager {
public Long2ObjectOpenHashMap>> getTicketsCopy() {
this.ticketLock.lock();
try {
- return this.tickets.clone();
+ // Folia start - region threading
+ Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>();
+ this.world.regioniser.computeForAllRegions((region) -> {
+ for (final LongIterator iterator = region.getData().getHolderManagerRegionData().tickets.keySet().longIterator(); iterator.hasNext();) {
+ final long chunk = iterator.nextLong();
+
+ ret.put(chunk, region.getData().getHolderManagerRegionData().tickets.get(chunk));
+ }
+ });
+ return ret;
+ // Folia end - region threading
} finally {
this.ticketLock.unlock();
}
@@ -322,7 +518,11 @@ public final class ChunkHolderManager {
ImmutableList.Builder ret;
this.ticketLock.lock();
try {
- SortedArraySet> tickets = this.tickets.get(ChunkPos.asLong(x, z));
+ // Folia start - region threading
+ final long coordinate = CoordinateUtils.getChunkKey(x, z);
+ final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate);
+ final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate);
+ // Folia end - region threading
if (tickets == null) {
return Collections.emptyList();
@@ -375,12 +575,37 @@ public final class ChunkHolderManager {
return false;
}
+ // Folia start - region threading
+ final ThreadedRegionizer.ThreadedRegion currRegion = TickRegionScheduler.getCurrentRegion();
+ final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised(
+ CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)
+ ) != currRegion;
+ // Folia end - region threading
+
this.ticketLock.lock();
try {
- final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : this.currentTick + removeDelay;
+ // Folia start - region threading
+ NewChunkHolder holder = this.chunkHolders.get(chunk);
+ if (holder == null) {
+ // we need to guarantee that a chunk holder exists for each ticket
+ // this must be executed before retrieving the holder manager data for a target chunk, to ensure the
+ // region will exist
+ this.chunkHolders.put(chunk, holder = this.createChunkHolder(chunk));
+ this.specialCaseUnload.add(holder);
+ }
+
+ if (lock) {
+ // we just need to prevent merging, so we only need the read lock
+ // additionally, this will prevent deadlock in the remove all tickets function by using the read lock
+ this.world.regioniser.acquireReadLock();
+ }
+ try {
+ final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData();
+ // Folia end - region threading
+ final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : targetData.currentTick + removeDelay; // Folia - region threading
final Ticket ticket = new Ticket<>(type, level, identifier, removeTick);
- final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
+ final SortedArraySet> ticketsAtChunk = targetData.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { // Folia - region threading
return SortedArraySet.create(4);
});
@@ -392,25 +617,25 @@ public final class ChunkHolderManager {
final long oldRemovalTick = current.removalTick;
if (removeTick != oldRemovalTick) {
if (oldRemovalTick != NO_TIMEOUT_MARKER) {
- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(oldRemovalTick);
+ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(oldRemovalTick); // Folia - region threading
final int prevCount = removeCounts.addTo(chunk, -1);
if (prevCount == 1) {
removeCounts.remove(chunk);
if (removeCounts.isEmpty()) {
- this.removeTickToChunkExpireTicketCount.remove(oldRemovalTick);
+ targetData.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); // Folia - region threading
}
}
}
if (removeTick != NO_TIMEOUT_MARKER) {
- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> {
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading
return new Long2IntOpenHashMap();
}).addTo(chunk, 1);
}
}
} else {
if (removeTick != NO_TIMEOUT_MARKER) {
- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> {
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading
return new Long2IntOpenHashMap();
}).addTo(chunk, 1);
}
@@ -421,6 +646,11 @@ public final class ChunkHolderManager {
}
return current == ticket;
+ } finally { // Folia start - region threading
+ if (lock) {
+ this.world.regioniser.releaseReadLock();
+ }
+ } // Folia end - region threading
} finally {
this.ticketLock.unlock();
}
@@ -439,35 +669,70 @@ public final class ChunkHolderManager {
return false;
}
+ // Folia start - region threading
+ final ThreadedRegionizer.ThreadedRegion currRegion = TickRegionScheduler.getCurrentRegion();
+ final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised(
+ CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)
+ ) != currRegion;
+ // Folia end - region threading
+
this.ticketLock.lock();
try {
- final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk);
+ // Folia start - region threading
+ if (lock) {
+ // we just need to prevent merging, so we only need the read lock
+ // additionally, this will prevent deadlock in the remove all tickets function by using the read lock
+ this.world.regioniser.acquireReadLock();
+ }
+ try {
+ final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData();
+ // Folia end - region threading
+
+ final SortedArraySet> ticketsAtChunk = targetData == null ? null : targetData.tickets.get(chunk);
+ // Folia end - region threading
if (ticketsAtChunk == null) {
return false;
}
final int oldLevel = getTicketLevelAt(ticketsAtChunk);
- final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, -2L));
+ final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, PROBE_MARKER)); // Folia - region threading
if (ticket == null) {
return false;
}
+ int newLevel = getTicketLevelAt(ticketsAtChunk); // Folia - region threading - moved up from below
+ // Folia start - region threading
+ // we should not change the ticket levels while the target region may be ticking
+ if (newLevel > level) {
+ final long unknownRemoveTick = targetData.currentTick + Math.max(0, TicketType.UNKNOWN.timeout);
+ final Ticket unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk), unknownRemoveTick);
+ if (ticketsAtChunk.add(unknownTicket)) {
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(unknownRemoveTick, (final long keyInMap) -> {
+ return new Long2IntOpenHashMap();
+ }).addTo(chunk, 1);
+ } else {
+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
+ }
+ newLevel = level;
+ }
+ // Folia end - region threading
+
if (ticketsAtChunk.isEmpty()) {
- this.tickets.remove(chunk);
+ targetData.tickets.remove(chunk); // Folia - region threading
}
- final int newLevel = getTicketLevelAt(ticketsAtChunk);
+ // Folia - region threading - move up
final long removeTick = ticket.removalTick;
if (removeTick != NO_TIMEOUT_MARKER) {
- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(removeTick);
+ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(removeTick); // Folia - region threading
final int currCount = removeCounts.addTo(chunk, -1);
if (currCount == 1) {
removeCounts.remove(chunk);
if (removeCounts.isEmpty()) {
- this.removeTickToChunkExpireTicketCount.remove(removeTick);
+ targetData.removeTickToChunkExpireTicketCount.remove(removeTick); // Folia - region threading
}
}
}
@@ -477,6 +742,11 @@ public final class ChunkHolderManager {
}
return true;
+ } finally { // Folia start - region threading
+ if (lock) {
+ this.world.regioniser.releaseReadLock();
+ }
+ } // Folia end - region threading
} finally {
this.ticketLock.unlock();
}
@@ -516,24 +786,33 @@ public final class ChunkHolderManager {
this.ticketLock.lock();
try {
- for (final LongIterator iterator = new LongArrayList(this.tickets.keySet()).longIterator(); iterator.hasNext();) {
- final long chunk = iterator.nextLong();
+ // Folia start - region threading
+ this.world.regioniser.computeForAllRegions((region) -> {
+ for (final LongIterator iterator = new LongArrayList(region.getData().getHolderManagerRegionData().tickets.keySet()).longIterator(); iterator.hasNext();) {
+ final long chunk = iterator.nextLong();
- this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier);
- }
+ this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier);
+ }
+ });
+ // Folia end - region threading
} finally {
this.ticketLock.unlock();
}
}
public void tick() {
- TickThread.ensureTickThread("Cannot tick ticket manager off-main");
+ // Folia start - region threading
+ final ChunkHolderManager.HolderManagerRegionData data = this.getCurrentRegionData();
+ if (data == null) {
+ throw new IllegalStateException("Not running tick() while on a region");
+ }
+ // Folia end - region threading
this.ticketLock.lock();
try {
- final long tick = ++this.currentTick;
+ final long tick = ++data.currentTick; // Folia - region threading
- final Long2IntOpenHashMap toRemove = this.removeTickToChunkExpireTicketCount.remove(tick);
+ final Long2IntOpenHashMap toRemove = data.removeTickToChunkExpireTicketCount.remove(tick); // Folia - region threading
if (toRemove == null) {
return;
@@ -546,10 +825,10 @@ public final class ChunkHolderManager {
for (final LongIterator iterator = toRemove.keySet().longIterator(); iterator.hasNext();) {
final long chunk = iterator.nextLong();
- final SortedArraySet> tickets = this.tickets.get(chunk);
+ final SortedArraySet> tickets = data.tickets.get(chunk); // Folia - region threading
tickets.removeIf(expireNow);
if (tickets.isEmpty()) {
- this.tickets.remove(chunk);
+ data.tickets.remove(chunk); // Folia - region threading
this.ticketLevelPropagator.removeSource(chunk);
} else {
this.ticketLevelPropagator.setSource(chunk, convertBetweenTicketLevels(tickets.first().getTicketLevel()));
@@ -798,30 +1077,62 @@ public final class ChunkHolderManager {
if (changedFullStatus.isEmpty()) {
return;
}
- if (!TickThread.isTickThread()) {
- this.taskScheduler.scheduleChunkTask(() -> {
- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
- }
- ChunkHolderManager.this.processPendingFullUpdate();
- }, PrioritisedExecutor.Priority.HIGHEST);
- } else {
- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>();
+ final List thisRegionHolders = new ArrayList<>();
+
+ final int regionShift = this.world.regioniser.sectionChunkShift;
+ final ThreadedRegionizer.ThreadedRegion thisRegion
+ = TickRegionScheduler.getCurrentRegion();
+
+ for (final NewChunkHolder holder : changedFullStatus) {
+ final int regionX = holder.chunkX >> regionShift;
+ final int regionZ = holder.chunkZ >> regionShift;
+ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ);
+
+ // region may be null
+ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) {
+ thisRegionHolders.add(holder);
+ } else {
+ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> {
+ return new ArrayList<>();
+ }).add(holder);
+ }
+ }
+
+ if (!thisRegionHolders.isEmpty()) {
+ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders);
+ }
+
+ if (!sectionToUpdates.isEmpty()) {
+ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+
+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
+
+ final List regionHolders = entry.getValue();
+ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { // Folia - region threading
+ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders);
+ ChunkHolderManager.this.processPendingFullUpdate();
+ }, PrioritisedExecutor.Priority.HIGHEST);
}
}
}
final ReferenceLinkedOpenHashSet unloadQueue = new ReferenceLinkedOpenHashSet<>();
+ /*
+ * Note: Only called on chunk holders that the current ticking region owns
+ */
private void removeChunkHolder(final NewChunkHolder holder) {
holder.killed = true;
holder.vanillaChunkHolder.onChunkRemove();
- this.autoSaveQueue.remove(holder);
+ // Folia - region threading
ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
+ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
}
@@ -839,6 +1150,8 @@ public final class ChunkHolderManager {
throw new IllegalStateException("Cannot hold scheduling lock while calling processUnloads");
}
+ final ChunkHolderManager.HolderManagerRegionData currentData = this.getCurrentRegionData(); // Folia - region threading
+
final List unloadQueue;
final List scheduleList = new ArrayList<>();
this.ticketLock.lock();
@@ -851,11 +1164,22 @@ public final class ChunkHolderManager {
// in order to ensure all chunks in the unload queue do not have a pending ticket level update,
// process them now
this.processTicketUpdates(false, false, scheduleList);
- unloadQueue = new ArrayList<>((int)(this.unloadQueue.size() * 0.05) + 1);
- final int unloadCount = Math.max(50, (int)(this.unloadQueue.size() * 0.05));
- for (int i = 0; i < unloadCount && !this.unloadQueue.isEmpty(); ++i) {
- final NewChunkHolder chunkHolder = this.unloadQueue.removeFirst();
+ // Folia start - region threading
+ final ArrayDeque toUnload = new ArrayDeque<>();
+ // The unload queue is globally maintained, but we can only unload chunks in our region
+ for (final NewChunkHolder holder : this.unloadQueue) {
+ if (TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
+ toUnload.add(holder);
+ }
+ }
+ // Folia end - region threading
+
+ final int unloadCount = Math.max(50, (int)(toUnload.size() * 0.05)); // Folia - region threading
+ unloadQueue = new ArrayList<>(unloadCount + 1); // Folia - region threading
+ for (int i = 0; i < unloadCount && !toUnload.isEmpty(); ++i) { // Folia - region threading
+ final NewChunkHolder chunkHolder = toUnload.removeFirst(); // Folia - region threading
+ this.unloadQueue.remove(chunkHolder); // Folia - region threading
if (chunkHolder.isSafeToUnload() != null) {
LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?");
continue;
@@ -1168,6 +1492,17 @@ public final class ChunkHolderManager {
}
this.ticketLevelUpdates.clear();
+
+ // Folia start - region threading
+ // it is possible that a special case new chunk holder had its ticket removed before it was propagated,
+ // which means checkUnload was never invoked. By checking unload here, we ensure that either the
+ // ticket level was propagated (in which case, a later depropagation would check again) or that
+ // we called checkUnload for it.
+ for (final NewChunkHolder special : this.specialCaseUnload) {
+ special.checkUnload();
+ }
+ this.specialCaseUnload.clear();
+ // Folia end - region threading
}
}
} finally {
@@ -1193,7 +1528,12 @@ public final class ChunkHolderManager {
// only call on tick thread
protected final boolean processPendingFullUpdate() {
- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+ final HolderManagerRegionData data = this.getCurrentRegionData();
+ if (data == null) {
+ return false;
+ }
+
+ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate;
boolean ret = false;
@@ -1204,9 +1544,7 @@ public final class ChunkHolderManager {
ret |= holder.handleFullStatusChange(changedFullStatus);
if (!changedFullStatus.isEmpty()) {
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
- }
+ this.addChangedStatuses(changedFullStatus);
changedFullStatus.clear();
}
}
@@ -1256,7 +1594,7 @@ public final class ChunkHolderManager {
private JsonObject getDebugJsonNoLock() {
final JsonObject ret = new JsonObject();
- ret.addProperty("current_tick", Long.valueOf(this.currentTick));
+ // Folia - region threading - move down
final JsonArray unloadQueue = new JsonArray();
ret.add("unload_queue", unloadQueue);
@@ -1275,60 +1613,73 @@ public final class ChunkHolderManager {
holders.add(holder.getDebugJson());
}
- final JsonArray removeTickToChunkExpireTicketCount = new JsonArray();
- ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount);
+ // Folia start - region threading
+ final JsonArray regions = new JsonArray();
+ ret.add("regions", regions);
+ this.world.regioniser.computeForAllRegionsUnsynchronised((region) -> {
+ final JsonObject regionJson = new JsonObject();
+ regions.add(regionJson);
+
+ final TickRegions.TickRegionData regionData = region.getData();
- for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) {
- final long tick = tickEntry.getLongKey();
- final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue();
+ regionJson.addProperty("current_tick", Long.valueOf(regionData.getCurrentTick()));
- final JsonObject tickJson = new JsonObject();
- removeTickToChunkExpireTicketCount.add(tickJson);
+ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray();
+ regionJson.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount);
- tickJson.addProperty("tick", Long.valueOf(tick));
+ for (final Long2ObjectMap.Entry tickEntry : regionData.getHolderManagerRegionData().removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) {
+ final long tick = tickEntry.getLongKey();
+ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue();
- final JsonArray tickEntries = new JsonArray();
- tickJson.add("entries", tickEntries);
+ final JsonObject tickJson = new JsonObject();
+ removeTickToChunkExpireTicketCount.add(tickJson);
- for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) {
- final long coordinate = entry.getLongKey();
- final int count = entry.getIntValue();
+ tickJson.addProperty("tick", Long.valueOf(tick));
- final JsonObject entryJson = new JsonObject();
- tickEntries.add(entryJson);
+ final JsonArray tickEntries = new JsonArray();
+ tickJson.add("entries", tickEntries);
- entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
- entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
- entryJson.addProperty("count", Integer.valueOf(count));
+ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) {
+ final long coordinate = entry.getLongKey();
+ final int count = entry.getIntValue();
+
+ final JsonObject entryJson = new JsonObject();
+ tickEntries.add(entryJson);
+
+ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+ entryJson.addProperty("count", Integer.valueOf(count));
+ }
}
- }
- final JsonArray allTicketsJson = new JsonArray();
- ret.add("tickets", allTicketsJson);
+ final JsonArray allTicketsJson = new JsonArray();
+ regionJson.add("tickets", allTicketsJson);
- for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) {
- final long coordinate = coordinateTickets.getLongKey();
- final SortedArraySet> tickets = coordinateTickets.getValue();
+ for (final Long2ObjectMap.Entry>> coordinateTickets : regionData.getHolderManagerRegionData().tickets.long2ObjectEntrySet()) {
+ final long coordinate = coordinateTickets.getLongKey();
+ final SortedArraySet> tickets = coordinateTickets.getValue();
- final JsonObject coordinateJson = new JsonObject();
- allTicketsJson.add(coordinateJson);
+ final JsonObject coordinateJson = new JsonObject();
+ allTicketsJson.add(coordinateJson);
- coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
- coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
- final JsonArray ticketsSerialized = new JsonArray();
- coordinateJson.add("tickets", ticketsSerialized);
+ final JsonArray ticketsSerialized = new JsonArray();
+ coordinateJson.add("tickets", ticketsSerialized);
- for (final Ticket> ticket : tickets) {
- final JsonObject ticketSerialized = new JsonObject();
- ticketsSerialized.add(ticketSerialized);
+ for (final Ticket> ticket : tickets) {
+ final JsonObject ticketSerialized = new JsonObject();
+ ticketsSerialized.add(ticketSerialized);
- ticketSerialized.addProperty("type", ticket.getType().toString());
- ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
- ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
- ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick));
+ ticketSerialized.addProperty("type", ticket.getType().toString());
+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
+ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick));
+ }
}
- }
+ });
+ // Folia end - region threading
return ret;
}
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..25db30284e3bab9ebad1ca7320db66116057e599 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
@@ -113,7 +113,7 @@ public final class ChunkTaskScheduler {
public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
+ // Folia - regionised ticking
final ReentrantLock schedulingLock = new ReentrantLock();
public final ChunkHolderManager chunkHolderManager;
@@ -240,14 +240,13 @@ public final class ChunkTaskScheduler {
};
// this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
- this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
+ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); // Folia - region threading
// so, make the main thread pick it up
MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
}
public boolean executeMainThreadTask() {
- TickThread.ensureTickThread("Cannot execute main thread task off-main");
- return this.mainThreadExecutor.executeTask();
+ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking
}
public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
@@ -267,7 +266,7 @@ public final class ChunkTaskScheduler {
public void scheduleTickingState(final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus toStatus,
final boolean addTicket, final PrioritisedExecutor.Priority priority,
final Consumer onComplete) {
- if (!TickThread.isTickThread()) {
+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) {
this.scheduleChunkTask(chunkX, chunkZ, () -> {
ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
}, priority);
@@ -380,9 +379,50 @@ public final class ChunkTaskScheduler {
});
}
+ // Folia start - region threading
+ // only appropriate to use with ServerLevel#syncLoadNonFull
+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final PrioritisedExecutor.Priority priority) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
+ final List tasks = new ArrayList<>();
+ this.chunkHolderManager.ticketLock.lock();
+ try {
+ this.schedulingLock.lock();
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ return false;
+ } else {
+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
+ return true;
+ } else {
+ chunkHolder.raisePriority(priority);
+
+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
+ }
+ }
+ }
+ } finally {
+ this.schedulingLock.unlock();
+ }
+ } finally {
+ this.chunkHolderManager.ticketLock.unlock();
+ }
+
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+
+ return true;
+ }
+ // Folia end - region threading
+
public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
final PrioritisedExecutor.Priority priority, final Consumer onComplete) {
- if (!TickThread.isTickThread()) {
+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) {
this.scheduleChunkTask(chunkX, chunkZ, () -> {
ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
}, priority);
@@ -409,7 +449,7 @@ public final class ChunkTaskScheduler {
this.chunkHolderManager.processTicketUpdates();
}
- final Consumer loadCallback = (final ChunkAccess chunk) -> {
+ final Consumer loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> {
try {
if (onComplete != null) {
onComplete.accept(chunk);
@@ -449,7 +489,9 @@ public final class ChunkTaskScheduler {
if (!chunkHolder.upgradeGenTarget(toStatus)) {
this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
}
- chunkHolder.addStatusConsumer(toStatus, loadCallback);
+ if (loadCallback != null) {
+ chunkHolder.addStatusConsumer(toStatus, loadCallback);
+ }
}
}
} finally {
@@ -463,7 +505,7 @@ public final class ChunkTaskScheduler {
tasks.get(i).schedule();
}
- if (!scheduled) {
+ if (loadCallback != null && !scheduled) {
// couldn't schedule
try {
loadCallback.accept(chunk);
@@ -652,7 +694,7 @@ public final class ChunkTaskScheduler {
*/
@Deprecated
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
- return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL);
+ throw new UnsupportedOperationException(); // Folia - regionised ticking
}
/**
@@ -660,7 +702,7 @@ public final class ChunkTaskScheduler {
*/
@Deprecated
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
- return this.mainThreadExecutor.queueRunnable(run, priority);
+ throw new UnsupportedOperationException(); // Folia - regionised ticking
}
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
@@ -669,28 +711,33 @@ public final class ChunkTaskScheduler {
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
final PrioritisedExecutor.Priority priority) {
- return this.mainThreadExecutor.createTask(run, priority);
+ return MinecraftServer.getServer().regionizedServer.taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
}
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
- return this.mainThreadExecutor.queueRunnable(run);
+ return this.scheduleChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); // TODO rebase into chunk system patch
}
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
final PrioritisedExecutor.Priority priority) {
- return this.mainThreadExecutor.queueRunnable(run, priority);
+ return MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
}
- public void executeTasksUntil(final BooleanSupplier exit) {
- if (Bukkit.isPrimaryThread()) {
- this.mainThreadExecutor.executeConditionally(exit);
- } else {
- long counter = 1L;
- while (!exit.getAsBoolean()) {
- counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms
- }
- }
+ // Folia start - region threading
+ // this function is guaranteed to never touch the ticket lock or schedule lock
+ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the
+ // ticket lock in the schedule logic
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run,
+ final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority);
+ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> {
+ MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority);
+ });
+ return ret;
}
+ // Folia end - region threading
+
+ // Folia - regionised ticking
public boolean halt(final boolean sync, final long maxWaitNS) {
this.lightExecutor.halt();
@@ -699,6 +746,7 @@ public final class ChunkTaskScheduler {
this.loadExecutor.halt();
final long time = System.nanoTime();
if (sync) {
+ // start at 10 * 0.5ms -> 5ms
for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
if (
!this.lightExecutor.isActive() &&
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
index 8013dd333e27aa5fd0beb431fa32491eec9f5246..3b70ccd8e0b1ada943f57faf99c23b2935249cf6 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
@@ -708,7 +708,7 @@ public final class NewChunkHolder {
boolean killed;
// must hold scheduling lock
- private void checkUnload() {
+ void checkUnload() { // Folia - region threading
if (this.killed) {
return;
}
@@ -1412,7 +1412,7 @@ public final class NewChunkHolder {
}
// must be scheduled to main, we do not trust the callback to not do anything stupid
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
for (final Consumer consumer : consumers) {
try {
consumer.accept(chunk);
@@ -1455,7 +1455,7 @@ public final class NewChunkHolder {
}
// must be scheduled to main, we do not trust the callback to not do anything stupid
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
for (final Consumer consumer : consumers) {
try {
consumer.accept(chunk);
@@ -1715,7 +1715,7 @@ public final class NewChunkHolder {
return this.entityChunk;
}
- public long lastAutoSave;
+ public long lastAutoSave; // Folia - region threaded - change to relative delay
public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
@@ -1865,7 +1865,7 @@ public final class NewChunkHolder {
} catch (final ThreadDeath death) {
throw death;
} catch (final Throwable thr) {
- LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
if (unloading && !completing) {
this.completeAsyncChunkDataSave(null);
}
@@ -1913,7 +1913,7 @@ public final class NewChunkHolder {
} catch (final ThreadDeath death) {
throw death;
} catch (final Throwable thr) {
- LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
}
return true;
@@ -1939,7 +1939,7 @@ public final class NewChunkHolder {
} catch (final ThreadDeath death) {
throw death;
} catch (final Throwable thr) {
- LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
}
return true;
diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
index 7ba60b4b4f29a42c58d98aafc5ea0fa3214f554c..dc1820e1231c15a7244883731a86f04fa8d20aa1 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -43,7 +43,7 @@ public final class PaperCommand extends Command {
commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
commands.put(Set.of("dumpitem"), new DumpItemCommand());
- commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
+ commands.put(Set.of("mobcaps"), new MobcapsCommand()); // Folia - region threading - revert per player mob caps
commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
return commands.entrySet().stream()
diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
index bbb8b1933ef33a3b91f69545f69dd3cfb84b27f5..b23b76a598731da8feef53c370b341233afdbea7 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
@@ -17,7 +17,7 @@ public final class PaperCommands {
private static final Map COMMANDS = new HashMap<>();
static {
COMMANDS.put("paper", new PaperCommand("paper"));
- COMMANDS.put("mspt", new MSPTCommand("mspt"));
+ COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Folia - region threading
}
public static void registerCommands(final MinecraftServer server) {
diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
index cd2e4d792e972b8bf1e07b8961594a670ae949cf..3ab8dbf2768a4ef8fb53af6f5431f7f6afe6d168 100644
--- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
+++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
@@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW;
public final class HeapDumpCommand implements PaperSubcommand {
@Override
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
this.dumpHeap(sender);
+ }); // Folia - region threading
return true;
}
diff --git a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
index 99c41a39cdad0271d089c6e03bebfdafba1aaa57..41aaa709dc2e474f23e759ebc51f33021c4f5485 100644
--- a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
+++ b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
@@ -46,7 +46,7 @@ public final class MobcapsCommand implements PaperSubcommand {
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
switch (subCommand) {
case "mobcaps" -> this.printMobcaps(sender, args);
- case "playermobcaps" -> this.printPlayerMobcaps(sender, args);
+ //case "playermobcaps" -> this.printPlayerMobcaps(sender, args); // Folia - region threading - revert per player mob caps
}
return true;
}
@@ -55,7 +55,7 @@ public final class MobcapsCommand implements PaperSubcommand {
public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
return switch (subCommand) {
case "mobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestMobcaps(args));
- case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args));
+ //case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args)); // Folia - region threading - revert per player mob caps
default -> throw new IllegalArgumentException();
};
}
@@ -140,41 +140,7 @@ public final class MobcapsCommand implements PaperSubcommand {
}
}
- private void printPlayerMobcaps(final CommandSender sender, final String[] args) {
- final @Nullable Player player;
- if (args.length == 0) {
- if (sender instanceof Player pl) {
- player = pl;
- } else {
- sender.sendMessage(Component.text("Must specify a player! ex: '/paper playermobcount playerName'", NamedTextColor.RED));
- return;
- }
- } else if (args.length == 1) {
- final String input = args[0];
- player = Bukkit.getPlayerExact(input);
- if (player == null) {
- sender.sendMessage(Component.text("Could not find player named '" + input + "'", NamedTextColor.RED));
- return;
- }
- } else {
- sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED));
- return;
- }
-
- final ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
- final ServerLevel level = serverPlayer.getLevel();
-
- if (!level.paperConfig().entities.spawning.perPlayerMobSpawns) {
- sender.sendMessage(Component.text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED));
- return;
- }
-
- sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), Component.text("Mobcaps for player: "), Component.text(player.getName(), NamedTextColor.GREEN)));
- sender.sendMessage(createMobcapsComponent(
- category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category),
- category -> level.getWorld().getSpawnLimitUnsafe(org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(category))
- ));
- }
+ // Folia - region threading - revert per player mob caps
private static Component createMobcapsComponent(final ToIntFunction countGetter, final ToIntFunction limitGetter) {
return MOB_CATEGORY_COLORS.entrySet().stream()
diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..48a43341b17247355a531164019d5cc9c5555f26 100644
--- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
+++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
@@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED;
public final class ReloadCommand implements PaperSubcommand {
@Override
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
this.doReload(sender);
+ }); // Folia - region threading
return true;
}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
index 9f5f0d8ddc8f480b48079c70e38c9c08eff403f6..3b83f25a24d6f9cdbf131d5a4432fb4ad018be4e 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -288,6 +288,18 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean strictAdvancementDimensionCheck = false;
}
+ // Folia start - threaded regions
+ public ThreadedRegions threadedRegions;
+ public class ThreadedRegions extends Post {
+
+ public int threads = -1;
+
+ @Override
+ public void postProcess() {
+ io.papermc.paper.threadedregions.TickRegions.init(this);
+ }
+ }
+ // Folia end - threaded regions
public ChunkLoadingBasic chunkLoadingBasic;
public class ChunkLoadingBasic extends ConfigurationPart {
diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
index 51cf0014c4229fc8671804d885b6381996810130..ee7b26d80ad3b4f76196492b9ffc11b33f15b9ad 100644
--- a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
@@ -128,7 +128,7 @@ public class WorldConfiguration extends ConfigurationPart {
public boolean filterBadTileEntityNbtFromFallingBlocks = true;
public List filteredEntityTagNbtPaths = NbtPathSerializer.fromString(List.of("Pos", "Motion", "SleepingX", "SleepingY", "SleepingZ"));
public boolean disableMobSpawnerSpawnEggTransformation = false;
- public boolean perPlayerMobSpawns = true;
+ //public boolean perPlayerMobSpawns = true; // Folia - region threading - revert per player mob caps
public boolean scanForLegacyEnderDragon = true;
@MergeMap
public Reference2IntMap spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
index c0e896343c22badd97c774c4ed1daa4e274f5d44..1bca4a629fe986e5afa47d8884dc44b2be800f32 100644
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
@@ -243,10 +243,19 @@ class PaperPluginInstanceManager {
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
+ // Folia start - region threading
try {
- this.server.getScheduler().cancelTasks(plugin);
+ this.server.getGlobalRegionScheduler().cancelTasks(plugin);
} catch (Throwable ex) {
- this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling global tasks for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+ // Folia end - region threading
+
+ try {
+ this.server.getAsyncScheduler().cancelTasks(plugin); // Folia - region threading
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " // Folia - region threading
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9687722e02dfd4088c7030abbf5008eb0a092c8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
@@ -0,0 +1,181 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.util.Validate;
+import io.papermc.paper.util.TickThread;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.world.entity.Entity;
+import org.bukkit.craftbukkit.entity.CraftEntity;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * An entity can move between worlds with an arbitrary tick delay, be temporarily removed
+ * for players (i.e end credits), be partially removed from world state (i.e inactive but not removed),
+ * teleport between ticking regions, teleport between worlds (which will change the underlying Entity object
+ * for non-players), and even be removed entirely from the server. The uncertainty of an entity's state can make
+ * it difficult to schedule tasks without worrying about undefined behaviors resulting from any of the states listed
+ * previously.
+ *
+ *
+ * This class is designed to eliminate those states by providing an interface to run tasks only when an entity
+ * is contained in a world, on the owning thread for the region, and by providing the current Entity object.
+ * The scheduler also allows a task to provide a callback, the "retired" callback, that will be invoked
+ * if the entity is removed before a task that was scheduled could be executed. The scheduler is also
+ * completely thread-safe, allowing tasks to be scheduled from any thread context. The scheduler also indicates
+ * properly whether a task was scheduled successfully (i.e scheduler not retired), thus the code scheduling any task
+ * knows whether the given callbacks will be invoked eventually or not - which may be critical for off-thread
+ * contexts.
+ *
+ */
+public final class EntityScheduler {
+
+ /**
+ * The Entity. Note that it is the CraftEntity, since only that class properly tracks world transfers.
+ */
+ public final CraftEntity entity;
+
+ private static final record ScheduledTask(Consumer extends Entity> run, Consumer extends Entity> retired) {}
+
+ private long tickCount = 0L;
+ private static final long RETIRED_TICK_COUNT = -1L;
+ private final Object stateLock = new Object();
+ private final Long2ObjectOpenHashMap> oneTimeDelayed = new Long2ObjectOpenHashMap<>();
+
+ private final ArrayDeque currentlyExecuting = new ArrayDeque<>();
+
+ public EntityScheduler(final CraftEntity entity) {
+ this.entity = Validate.notNull(entity);
+ }
+
+ /**
+ * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback
+ * on all currently scheduled tasks.
+ *
+ *
+ * Note: This should only be invoked after synchronously removing the entity from the world.
+ *
+ *
+ * @throws IllegalStateException If the scheduler is already retired.
+ */
+ public void retire() {
+ synchronized (this.stateLock) {
+ if (this.tickCount == RETIRED_TICK_COUNT) {
+ throw new IllegalStateException("Already retired");
+ }
+ this.tickCount = RETIRED_TICK_COUNT;
+ }
+
+ final Entity thisEntity = this.entity.getHandle();
+
+ // correctly handle and order retiring while running executeTick
+ for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) {
+ final ScheduledTask task = this.currentlyExecuting.pollFirst();
+ final Consumer retireTask = (Consumer)task.retired;
+ if (retireTask == null) {
+ continue;
+ }
+
+ retireTask.accept(thisEntity);
+ }
+
+ for (final List tasks : this.oneTimeDelayed.values()) {
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ final ScheduledTask task = tasks.get(i);
+ final Consumer retireTask = (Consumer)task.retired;
+ if (retireTask == null) {
+ continue;
+ }
+
+ retireTask.accept(thisEntity);
+ }
+ }
+ }
+
+ /**
+ * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity
+ * removed), then returns {@code false}. Otherwise, either the run callback will be invoked after the specified delay,
+ * or the retired callback will be invoked if the scheduler is retired.
+ * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove
+ * other entities, load chunks, load worlds, modify ticket levels, etc.
+ *
+ *
+ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity.
+ *
+ *
+ * The run and retired callback take an Entity parameter representing the current object entity that the scheduler
+ * is tied to. Since the scheduler is transferred when an entity changes dimensions, it is possible the entity parameter
+ * is not the same when the task was first scheduled. Thus, only the parameter provided should be used.
+ *
+ * @param run The callback to run after the specified delay, may not be null.
+ * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null.
+ * @param delay The delay in ticks before the run callback is invoked. Any value less-than 1 is treated as 1.
+ * @return {@code true} if the task was scheduled, which means that either the run function or the retired function
+ * will be invoked (but never both), or {@code false} indicating neither the run nor retired function will be invoked
+ * since the scheduler has been retired.
+ */
+ public boolean schedule(final Consumer extends Entity> run, final Consumer extends Entity> retired, final long delay) {
+ Validate.notNull(run, "Run task may not be null");
+
+ final ScheduledTask task = new ScheduledTask(run, retired);
+ synchronized (this.stateLock) {
+ if (this.tickCount == RETIRED_TICK_COUNT) {
+ return false;
+ }
+ this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> {
+ return new ArrayList<>();
+ }).add(task);
+ }
+
+ return true;
+ }
+
+ /**
+ * Executes a tick for the scheduler.
+ *
+ * @throws IllegalStateException If the scheduler is retired.
+ */
+ public void executeTick() {
+ final Entity thisEntity = this.entity.getHandle();
+
+ TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously");
+ final List toRun;
+ synchronized (this.stateLock) {
+ if (this.tickCount == RETIRED_TICK_COUNT) {
+ throw new IllegalStateException("Ticking retired scheduler");
+ }
+ ++this.tickCount;
+ if (this.oneTimeDelayed.isEmpty()) {
+ toRun = null;
+ } else {
+ toRun = this.oneTimeDelayed.remove(this.tickCount);
+ }
+ }
+
+ if (toRun != null) {
+ for (int i = 0, len = toRun.size(); i < len; ++i) {
+ this.currentlyExecuting.addLast(toRun.get(i));
+ }
+ }
+
+ // Note: It is allowed for the tasks executed to retire the entity in a given task.
+ for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) {
+ if (!TickThread.isTickThreadFor(thisEntity)) {
+ // tp has been queued sync by one of the tasks
+ // in this case, we need to delay the tasks for next tick
+ break;
+ }
+ final ScheduledTask task = this.currentlyExecuting.pollFirst();
+
+ if (this.tickCount != RETIRED_TICK_COUNT) {
+ ((Consumer)task.run).accept(thisEntity);
+ } else {
+ // retired synchronously
+ // note: here task is null
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c288d1db81c4d6f574674889a98553baf9b86ee
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java
@@ -0,0 +1,174 @@
+package io.papermc.paper.threadedregions;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public final class RegionShutdownThread extends TickThread {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ ThreadedRegionizer.ThreadedRegion shuttingDown;
+
+ public RegionShutdownThread(final String name) {
+ super(name);
+ this.setUncaughtExceptionHandler((thread, thr) -> {
+ LOGGER.error("Error shutting down server", thr);
+ });
+ }
+
+ static ThreadedRegionizer.ThreadedRegion getRegion() {
+ final Thread currentThread = Thread.currentThread();
+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
+ return shutdownThread.shuttingDown;
+ }
+ return null;
+ }
+
+
+ static RegionizedWorldData getWorldData() {
+ final Thread currentThread = Thread.currentThread();
+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
+ // no fast path for shutting down
+ if (shutdownThread.shuttingDown != null) {
+ return shutdownThread.shuttingDown.getData().world.worldRegionData.get();
+ }
+ }
+ return null;
+ }
+
+ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves
+ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because
+ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not.
+ // Thus, the only operation that the shutdown thread will perform
+
+ private void saveLevelData(final ServerLevel world) {
+ try {
+ world.saveLevelData();
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr);
+ }
+ }
+
+ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region,
+ final ServerLevel world) {
+ try {
+ this.shuttingDown = region;
+ final List pendingTeleports = world.removeAllRegionTeleports();
+ if (pendingTeleports.isEmpty()) {
+ return;
+ }
+ final ChunkPos center = region.getCenterChunk();
+ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
+ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) {
+ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to());
+
+ // first, add entities to entity chunk so that they will be saved
+ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) {
+ // assume that world and position are set to destination here
+ node.root.level = world; // in case the pending teleport is from a portal before it finds the exact destination
+ world.getEntityLookup().addEntityForShutdownTeleportComplete(node.root);
+ }
+
+ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if
+ // there are any player passengers, that the later player saving will save the tree
+ pendingTeleport.rootVehicle().restore();
+
+ // now we are finished
+ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to());
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to complete pending teleports", thr);
+ } finally {
+ this.shuttingDown = null;
+ }
+ }
+
+ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region,
+ final boolean last) {
+ ChunkPos center = null;
+ try {
+ this.shuttingDown = region;
+ center = region.getCenterChunk();
+ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
+ region.regioniser.world.chunkTaskScheduler.chunkHolderManager.close(true, true, false, last, false);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
+ } finally {
+ this.shuttingDown = null;
+ }
+ }
+
+ private void haltChunkSystem(final ServerLevel world) {
+ try {
+ world.chunkTaskScheduler.chunkHolderManager.close(false, true, true, false, false);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr);
+ }
+ }
+
+ private void haltWorldNoRegions(final ServerLevel world) {
+ try {
+ world.chunkTaskScheduler.chunkHolderManager.close(true, true, true, true, false);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to close world '" + world.getWorld().getName() + "' with no regions", thr);
+ }
+ }
+
+ @Override
+ public final void run() {
+ // await scheduler termination
+ LOGGER.info("Awaiting scheduler termination for 60s");
+ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) {
+ LOGGER.info("Scheduler halted");
+ } else {
+ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways");
+ }
+
+ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc
+ // halt all chunk systems first so that any in-progress chunk generation stops
+ LOGGER.info("Halting chunk systems");
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
+ try {
+ world.chunkTaskScheduler.halt(false, 0L);
+ } catch (final Throwable throwable) {
+ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable);
+ }
+ }
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
+ this.haltChunkSystem(world);
+ }
+ LOGGER.info("Halted chunk systems");
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
+ final List>
+ regions = new ArrayList<>();
+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
+
+ for (int i = 0, len = regions.size(); i < len; ++i) {
+ final ThreadedRegionizer.ThreadedRegion region = regions.get(i);
+ this.finishTeleportations(region, world);
+ }
+
+ for (int i = 0, len = regions.size(); i < len; ++i) {
+ final ThreadedRegionizer.ThreadedRegion region = regions.get(i);
+ this.saveRegionChunks(region, (i + 1) == len);
+ }
+
+ this.saveLevelData(world);
+ }
+ // moved from stop part 1
+ // we need this to be after saving level data, as that will complete any teleportations the player is in
+ LOGGER.info("Saving players");
+ MinecraftServer.getServer().getPlayerList().saveAll();
+
+ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc)
+ // done, part 2 should call exit()
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java
@@ -0,0 +1,235 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.util.Validate;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.server.level.ServerLevel;
+import javax.annotation.Nullable;
+import java.util.function.Supplier;
+
+/**
+ * Use to manage data that needs to be regionised.
+ *
+ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd.
+ * The data is held in reference to the world it resides in.
+ *
+ *
+ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region.
+ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced
+ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions.
+ * See below for more details on instancing per world.
+ *
+ *
+ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current
+ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below
+ * see why the behavior may or may not be desirable:
+ *
+ * {@code
+ * public class EntityTickList {
+ * private final List entities = new ArrayList<>();
+ *
+ * public void addEntity(Entity e) {
+ * this.entities.add(e);
+ * }
+ *
+ * public void removeEntity(Entity e) {
+ * this.entities.remove(e);
+ * }
+ * }
+ *
+ * public class World {
+ *
+ * // callback is left out of this example
+ * // note: world != null here
+ * public final RegionizedData entityTickLists =
+ * new RegionizedData<>(this, () -> new EntityTickList(), ...);
+ *
+ * public void addTickingEntity(Entity e) {
+ * // What we expect here is that this world is the
+ * // current ticking region's world.
+ * // If that is true, then calling this.entityTickLists.get()
+ * // will retrieve the current region's EntityTickList
+ * // for this world, which is fine since the current
+ * // region is contained within this world.
+ *
+ * // But if the current region's world is not this world,
+ * // and if the world check is disabled, then we will actually
+ * // retrieve _this_ world's EntityTickList for the region,
+ * // and NOT the EntityTickList for the region's world.
+ * // This is because the RegionizedData object is instantiated
+ * // per world.
+ * this.entityTickLists.get().addEntity(e);
+ * }
+ * }
+ *
+ * public class TickTimes {
+ *
+ * private final List tickTimesNS = new ArrayList<>();
+ *
+ * public void completeTick(long timeNS) {
+ * this.tickTimesNS.add(timeNS);
+ * }
+ *
+ * public double getAverageTickLengthMS() {
+ * double sum = 0.0;
+ * for (long time : tickTimesNS) {
+ * sum += (double)time;
+ * }
+ * return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
+ * }
+ * }
+ *
+ * public class Server {
+ * public final List worlds = ...;
+ *
+ * // callback is left out of this example
+ * // note: world == null here, because this RegionizedData object
+ * // is not instantiated per world, but rather globally.
+ * public final RegionizedData tickTimes =
+ * new RegionizedData<>(null, () -> new TickTimes(), ...);
+ * }
+ * }
+ *
+ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking
+ * is enabled for it by passing the world to the constructor.
+ *
+ */
+public final class RegionizedData {
+
+ private final ServerLevel world;
+ private final Supplier initialValueSupplier;
+ private final RegioniserCallback callback;
+
+ /**
+ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must
+ * never produce {@code null} values.
+ *
+ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking
+ * operations may deadlock the entire server and as such the function should be completely non-blocking
+ * and must complete in a timely manner.
+ *
+ *
+ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever
+ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null}
+ * for an entity tick list is invalid since the entities are tied to a world and region,
+ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to
+ * region only.
+ *
+ * @param world The world in which the region data resides.
+ * @param supplier Initial value supplier used to lazy initialise region data.
+ * @param callback Region callback to manage this regionised data.
+ */
+ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) {
+ this.world = world;
+ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null.");
+ this.callback = Validate.notNull(callback, "Regioniser callback may not be null.");
+ }
+
+ T createNewValue() {
+ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null");
+ }
+
+ RegioniserCallback getCallback() {
+ return this.callback;
+ }
+
+ /**
+ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}.
+ * @return the current data type for the current ticking region. If there is no region, returns {@code null}.
+ * @throws IllegalStateException If the following are true: The server is in region ticking mode,
+ * this {@code RegionizedData}'s world is not {@code null},
+ * and the current ticking region's world does not match this {@code RegionizedData}'s world.
+ */
+ public @Nullable T get() {
+ final ThreadedRegionizer.ThreadedRegion region =
+ TickRegionScheduler.getCurrentRegion();
+
+ if (region == null) {
+ return null;
+ }
+
+ if (this.world != null && this.world != region.getData().world) {
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
+ }
+
+ return region.getData().getOrCreateRegionizedData(this);
+ }
+
+ /**
+ * Class responsible for handling merge / split requests from the regioniser.
+ *
+ * It is critical to note that each function is called while holding the region lock.
+ *
+ */
+ public static interface RegioniserCallback {
+
+ /**
+ * Completely merges the data in {@code from} to {@code into}.
+ *
+ * Calculating Tick Offsets:
+ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines
+ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in
+ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from
+ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for
+ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from}
+ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom},
+ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and
+ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate
+ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}.
+ *
+ *
+ * Critical Notes:
+ *
+ *
+ * This function is called while the region lock is held, so any blocking operations may
+ * deadlock the entire server and as such the function should be completely non-blocking and must complete
+ * in a timely manner.
+ *
+ *
+ * This function may not throw any exceptions, or the server will be left in an unrecoverable state.
+ *
+ *
+ *
+ *
+ * @param from The data to merge from.
+ * @param into The data to merge into.
+ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region.
+ */
+ public void merge(final T from, final T into, final long fromTickOffset);
+
+ /**
+ * Splits the data in {@code from} into {@code dataSet}.
+ *
+ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}.
+ * To convert from chunk coordinates to region coordinates and keys, see the code below:
+ *
+ * {@code
+ * int chunkX = ...;
+ * int chunkZ = ...;
+ *
+ * int regionSectionX = chunkX >> chunkToRegionShift;
+ * int regionSectionZ = chunkZ >> chunkToRegionShift;
+ * long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
+ * }
+ *
+ *
+ *
+ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the
+ * data that is owned by the region which occupies the region section.
+ *
+ *
+ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because
+ * the new regions formed from the split will start at the same tick number, and so no adjustment is required.
+ *
+ *
+ * @param from The data to split from.
+ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates.
+ * @param regionToData Lookup hash table from region section key to .
+ * @param dataSet The data set to split into.
+ */
+ public void split(
+ final T from, final int chunkToRegionShift,
+ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet
+ );
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c1d55144f044f39926ddf998104950b9efe3ee1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java
@@ -0,0 +1,348 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler;
+import io.papermc.paper.util.TickThread;
+import net.minecraft.CrashReport;
+import net.minecraft.ReportedException;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketListener;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.network.protocol.game.ClientboundDisconnectPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.dedicated.DedicatedServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.server.network.ServerLoginPacketListenerImpl;
+import net.minecraft.world.level.GameRules;
+import org.bukkit.Bukkit;
+import org.slf4j.Logger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+
+public final class RegionizedServer {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final RegionizedServer INSTANCE = new RegionizedServer();
+
+ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue();
+
+ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>();
+
+ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>();
+
+ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this);
+
+ public static RegionizedServer getInstance() {
+ return INSTANCE;
+ }
+
+ public void addConnection(final Connection conn) {
+ this.connections.add(conn);
+ }
+
+ private boolean removeConnection(final Connection conn) {
+ return this.connections.remove(conn);
+ }
+
+ public void addWorld(final ServerLevel world) {
+ this.worlds.add(world);
+ }
+
+ public void init() {
+ // call init event _before_ scheduling anything
+ new RegionizedServerInitEvent().callEvent();
+
+ // now we can schedule
+ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
+ TickRegions.getScheduler().scheduleRegion(this.tickHandle);
+ TickRegions.getScheduler().init();
+ }
+
+ public void invalidateStatus() {
+ this.lastServerStatus = 0L;
+ }
+
+ public void addTaskWithoutNotify(final Runnable run) {
+ this.globalTickQueue.add(run);
+ }
+
+ public void addTask(final Runnable run) {
+ this.addTaskWithoutNotify(run);
+ TickRegions.getScheduler().setHasTasks(this.tickHandle);
+ }
+
+ /**
+ * Returns the current tick of the region ticking.
+ * @throws IllegalStateException If there is no current region.
+ */
+ public static long getCurrentTick() throws IllegalStateException {
+ final ThreadedRegionizer.ThreadedRegion region =
+ TickRegionScheduler.getCurrentRegion();
+ if (region == null) {
+ if (TickThread.isShutdownThread()) {
+ return 0L;
+ }
+ throw new IllegalStateException("No currently ticking region");
+ }
+ return region.getData().getCurrentTick();
+ }
+
+ public static boolean isGlobalTickThread() {
+ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask();
+ }
+
+ public static void ensureGlobalTickThread(final String reason) {
+ if (!isGlobalTickThread()) {
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() {
+ return INSTANCE.tickHandle;
+ }
+
+ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle {
+
+ private final RegionizedServer server;
+
+ private final AtomicBoolean scheduled = new AtomicBoolean();
+ private final AtomicBoolean ticking = new AtomicBoolean();
+
+ public GlobalTickTickHandle(final RegionizedServer server) {
+ super(null, SchedulerThreadPool.DEADLINE_NOT_SET);
+ this.server = server;
+ }
+
+ /**
+ * Only valid to call BEFORE scheduled!!!!
+ */
+ final void setInitialStart(final long start) {
+ if (this.scheduled.getAndSet(true)) {
+ throw new IllegalStateException("Double scheduling global tick");
+ }
+ this.updateScheduledStart(start);
+ }
+
+ @Override
+ protected boolean tryMarkTicking() {
+ return !this.ticking.getAndSet(true);
+ }
+
+ @Override
+ protected boolean markNotTicking() {
+ return this.ticking.getAndSet(false);
+ }
+
+ @Override
+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) {
+ this.drainTasks();
+ this.server.globalTick(tickCount);
+ }
+
+ private void drainTasks() {
+ while (this.runOneTask());
+ }
+
+ private boolean runOneTask() {
+ final Runnable run = this.server.globalTickQueue.poll();
+ if (run == null) {
+ return false;
+ }
+
+ // TODO try catch?
+ run.run();
+
+ return true;
+ }
+
+ @Override
+ protected boolean runRegionTasks(final BooleanSupplier canContinue) {
+ do {
+ if (!this.runOneTask()) {
+ return false;
+ }
+ } while (canContinue.getAsBoolean());
+
+ return true;
+ }
+
+ @Override
+ protected boolean hasIntermediateTasks() {
+ return !this.server.globalTickQueue.isEmpty();
+ }
+ }
+
+ private long lastServerStatus;
+ private long tickCount;
+
+ private void globalTick(final int tickCount) {
+ ++this.tickCount;
+ // expire invalid click command callbacks
+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount);
+
+ // scheduler
+ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick();
+
+ // commands
+ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs();
+
+ // needs
+ // player ping sample
+ // world global tick
+ // connection tick
+
+ // tick player ping sample
+ this.tickPlayerSample();
+
+ // tick worlds
+ for (final ServerLevel world : this.worlds) {
+ this.globalTick(world, tickCount);
+ }
+
+ // tick connections
+ this.tickConnections();
+
+ // player list
+ MinecraftServer.getServer().getPlayerList().tick();
+ }
+
+ private void tickPlayerSample() {
+ final MinecraftServer mcServer = MinecraftServer.getServer();
+
+ final long currtime = System.nanoTime();
+
+ // player ping sample
+ // copied from MinecraftServer#tickServer
+ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing
+ // an uncomplete status
+ if (currtime - this.lastServerStatus >= 5000000000L) {
+ this.lastServerStatus = currtime;
+ mcServer.rebuildServerStatus();
+ }
+ }
+
+ private boolean hasConnectionMovedToMain(final Connection conn) {
+ final PacketListener packetListener = conn.getPacketListener();
+
+ return (packetListener instanceof ServerGamePacketListenerImpl) ||
+ (packetListener instanceof ServerLoginPacketListenerImpl loginListener && loginListener.state.ordinal() >= ServerLoginPacketListenerImpl.State.HANDING_OFF.ordinal());
+ }
+
+ private void tickConnections() {
+ final List connections = new ArrayList<>(this.connections);
+ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging
+ for (final Connection conn : connections) {
+ if (!conn.becomeActive()) {
+ continue;
+ }
+
+ if (this.hasConnectionMovedToMain(conn)) {
+ if (!conn.isConnected()) {
+ this.removeConnection(conn);
+ }
+ continue;
+ }
+
+ if (!conn.isConnected()) {
+ this.removeConnection(conn);
+ conn.handleDisconnection();
+ continue;
+ }
+
+ try {
+ conn.tick();
+ } catch (final Exception exception) {
+ if (conn.isMemoryConnection()) {
+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
+ }
+
+ LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper
+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
+
+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
+ conn.disconnect(ichatmutablecomponent);
+ }));
+ conn.setReadOnly();
+ continue;
+ }
+ }
+ }
+
+ // A global tick only updates things like weather / worldborder, basically anything in the world that is
+ // NOT tied to a specific region, but rather shared amongst all of them.
+ private void globalTick(final ServerLevel world, final int tickCount) {
+ // needs
+ // worldborder tick
+ // advancing the weather cycle
+ // sleep status thing
+ // updating sky brightness
+ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking
+
+ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However,
+ // this may not be the case - and thus, only the global tick thread can do anything.
+ world.taskQueueRegionData.drainGlobalChunkTasks();
+
+ // worldborder tick
+ this.tickWorldBorder(world);
+
+ // weather cycle
+ this.advanceWeatherCycle(world);
+
+ // sleep status
+ this.checkNightSkip(world);
+
+ // update raids
+ this.updateRaids(world);
+
+ // sky brightness
+ this.updateSkyBrightness(world);
+
+ // time ticking (TODO API synchronisation?)
+ this.tickTime(world, tickCount);
+
+ world.updateTickData();
+ }
+
+ private void updateRaids(final ServerLevel world) {
+ world.getRaids().globalTick();
+ }
+
+ private void checkNightSkip(final ServerLevel world) {
+ world.tickSleep();
+ }
+
+ private void advanceWeatherCycle(final ServerLevel world) {
+ world.advanceWeatherCycle();
+ }
+
+ private void updateSkyBrightness(final ServerLevel world) {
+ world.updateSkyBrightness();
+ }
+
+ private void tickWorldBorder(final ServerLevel world) {
+ world.getWorldBorder().tick();
+ }
+
+ private void tickTime(final ServerLevel world, final int tickCount) {
+ if (world.tickTime) {
+ if (world.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
+ world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
+ }
+ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount);
+ }
+ }
+
+ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) {
+
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac043fbc74874c205b821c3d2d011b921d5704fa
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
@@ -0,0 +1,752 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
+import io.papermc.paper.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.Unit;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+
+public final class RegionizedTaskQueue {
+
+ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0);
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.queueChunkTask(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority);
+ ret.queue();
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority);
+ ret.queue();
+ return ret;
+ }
+
+ public static final class WorldRegionTaskData {
+ private final ServerLevel world;
+ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>();
+ private final SWMRLong2ObjectHashTable referenceCounters = new SWMRLong2ObjectHashTable<>();
+
+ public WorldRegionTaskData(final ServerLevel world) {
+ this.world = world;
+ }
+
+ private boolean executeGlobalChunkTask() {
+ final Runnable run = this.globalChunkTask.poll();
+ if (run != null) {
+ run.run();
+ return true;
+ }
+ return false;
+ }
+
+ public void drainGlobalChunkTasks() {
+ while (this.executeGlobalChunkTask());
+ }
+
+ public void pushGlobalChunkTask(final Runnable run) {
+ this.globalChunkTask.add(run);
+ }
+
+ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) {
+ final ThreadedRegionizer regioniser = this.world.regioniser;
+ final ThreadedRegionizer.ThreadedRegion region
+ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
+ if (region == null) {
+ return null;
+ }
+ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData();
+ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue);
+ }
+
+ private void removeTicket(final long coord) {
+ this.world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void addTicket(final long coord) {
+ this.world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void decrementReference(final AtomicLong reference, final long coord) {
+ final long val = reference.decrementAndGet();
+ if (val == 0L) {
+ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock;
+ ticketLock.lock();
+ try {
+ if (this.referenceCounters.remove(coord, reference)) {
+ WorldRegionTaskData.this.removeTicket(coord);
+ } // else: race condition, something replaced our reference - not our issue anymore
+ } finally {
+ ticketLock.unlock();
+ }
+ } else if (val < 0L) {
+ throw new IllegalStateException("Reference count < 0: " + val);
+ }
+ }
+
+ private AtomicLong incrementReference(final long coord) {
+ final AtomicLong ret = this.referenceCounters.get(coord);
+ if (ret != null) {
+ // try to fast acquire counter
+ int failures = 0;
+ for (long curr = ret.get();;) {
+ if (curr == 0L) {
+ // failed to fast acquire as reference expired
+ break;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = ret.compareAndExchange(curr, curr + 1L))) {
+ return ret;
+ }
+
+ ++failures;
+ }
+ }
+
+ // slow acquire
+ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock;
+ ticketLock.lock();
+ try {
+ final AtomicLong replace = new AtomicLong(1L);
+ final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(coord, replace);
+ if (valueInMap == null) {
+ // replaced, we should usually be here
+ this.addTicket(coord);
+ return replace;
+ } // else: need to attempt to acquire the reference
+
+ int failures = 0;
+ for (long curr = valueInMap.get();;) {
+ if (curr == 0L) {
+ // don't need to add ticket here, since ticket is only removed during the lock
+ // we just need to replace the value in the map so that the thread removing fails and doesn't
+ // remove the ticket (see decrementReference)
+ this.referenceCounters.put(coord, replace);
+ return replace;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) {
+ // acquired
+ return valueInMap;
+ }
+
+ ++failures;
+ }
+ } finally {
+ ticketLock.unlock();
+ }
+ }
+ }
+
+ public static final class RegionTaskQueueData {
+ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue();
+ private final PrioritisedQueue chunkQueue = new PrioritisedQueue();
+ private final WorldRegionTaskData worldRegionTaskData;
+
+ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) {
+ this.worldRegionTaskData = worldRegionTaskData;
+ }
+
+ void mergeInto(final RegionTaskQueueData into) {
+ this.tickTaskQueue.mergeInto(into.tickTaskQueue);
+ this.chunkQueue.mergeInto(into.chunkQueue);
+ }
+
+ public boolean executeTickTask() {
+ return this.tickTaskQueue.executeTask();
+ }
+
+ public boolean executeChunkTask() {
+ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask();
+ }
+
+ void split(final ThreadedRegionizer regioniser,
+ final Long2ReferenceOpenHashMap> into) {
+ this.tickTaskQueue.split(
+ false, regioniser, into
+ );
+ this.chunkQueue.split(
+ true, regioniser, into
+ );
+ }
+
+ public void drainTasks() {
+ // first, update chunk loader
+ final ServerLevel world = this.worldRegionTaskData.world;
+ if (world.playerChunkLoader.tickMidTick()) {
+ // only process ticket updates if the player chunk loader did anything
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
+ }
+
+ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue;
+ final PrioritisedQueue chunkTaskQueue = this.chunkQueue;
+
+ int allowedTickTasks = tickTaskQueue.getScheduledTasks();
+ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks();
+
+ boolean executeTickTasks = allowedTickTasks > 0;
+ boolean executeChunkTasks = allowedChunkTasks > 0;
+ boolean executeGlobalTasks = true;
+
+ do {
+ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask();
+ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask();
+ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask();
+ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks);
+
+ if (allowedChunkTasks > 0) {
+ // if we executed chunk tasks, we should try to process ticket updates for full status changes
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
+ }
+ }
+
+ public boolean hasTasks() {
+ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty();
+ }
+ }
+
+ static final class PrioritisedQueue {
+ private final ArrayDeque[] queues = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
+ for (int i = 0; i < PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
+ this.queues[i] = new ArrayDeque<>();
+ }
+ }
+ private boolean isDestroyed;
+
+ public int getScheduledTasks() {
+ synchronized (this) {
+ int ret = 0;
+
+ for (final ArrayDeque queue : this.queues) {
+ ret += queue.size();
+ }
+
+ return ret;
+ }
+ }
+
+ public boolean isEmpty() {
+ final ArrayDeque[] queues = this.queues;
+ final int max = PrioritisedExecutor.Priority.IDLE.priority;
+ synchronized (this) {
+ for (int i = 0; i <= max; ++i) {
+ if (!queues[i].isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public void mergeInto(final PrioritisedQueue target) {
+ synchronized (this) {
+ this.isDestroyed = true;
+ synchronized (target) {
+ mergeInto(target, this.queues);
+ }
+ }
+ }
+
+ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) {
+ final ArrayDeque[] otherQueues = target.queues;
+ for (int i = 0; i < thisQueues.length; ++i) {
+ final ArrayDeque fromQ = thisQueues[i];
+ final ArrayDeque intoQ = otherQueues[i];
+
+ // it is possible for another thread to queue tasks into the target queue before we do
+ // since only the ticking region can poll, we don't have to worry about it when they are being queued -
+ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes)
+ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the
+ // front of the queue, but we need to use descending iterator to ensure we do not reverse
+ // the order of elements from fromQ
+ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) {
+ intoQ.addFirst(iterator.next());
+ }
+ }
+ }
+
+ // into is a map of section coordinate to region
+ public void split(final boolean isChunkData,
+ final ThreadedRegionizer regioniser,
+ final Long2ReferenceOpenHashMap> into) {
+ final Reference2ReferenceOpenHashMap, ArrayDeque[]>
+ split = new Reference2ReferenceOpenHashMap<>();
+ final int shift = regioniser.sectionChunkShift;
+ synchronized (this) {
+ this.isDestroyed = true;
+ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting
+
+ // first, build the targets
+ final ArrayDeque[] thisQueues = this.queues;
+ for (int i = 0; i < thisQueues.length; ++i) {
+ final ArrayDeque fromQ = thisQueues[i];
+
+ for (final ChunkBasedPriorityTask task : fromQ) {
+ final int sectionX = task.chunkX >> shift;
+ final int sectionZ = task.chunkZ >> shift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final ThreadedRegionizer.ThreadedRegion
+ region = into.get(sectionKey);
+ if (region == null) {
+ throw new IllegalStateException();
+ }
+
+ split.computeIfAbsent(region, (keyInMap) -> {
+ final ArrayDeque[] ret = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES];
+
+ for (int k = 0; k < ret.length; ++k) {
+ ret[k] = new ArrayDeque<>();
+ }
+
+ return ret;
+ })[i].add(task);
+ }
+ }
+
+ // merge the targets into their queues
+ for (final Iterator, ArrayDeque[]>>
+ iterator = split.reference2ReferenceEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Reference2ReferenceMap.Entry, ArrayDeque[]>
+ entry = iterator.next();
+ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData();
+ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue());
+ }
+ }
+ }
+
+ /**
+ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true
+ * if the task was added
+ */
+ private Boolean tryPush(final ChunkBasedPriorityTask task) {
+ final ArrayDeque[] queues = this.queues;
+ synchronized (this) {
+ final PrioritisedExecutor.Priority priority = task.getPriority();
+ if (priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return null;
+ }
+ if (this.isDestroyed) {
+ return Boolean.FALSE;
+ }
+ queues[priority.priority].addLast(task);
+ return Boolean.TRUE;
+ }
+ }
+
+ private boolean executeTask() {
+ final ArrayDeque[] queues = this.queues;
+ final int max = PrioritisedExecutor.Priority.IDLE.priority;
+ ChunkBasedPriorityTask task = null;
+ AtomicLong referenceCounter = null;
+ synchronized (this) {
+ if (this.isDestroyed) {
+ throw new IllegalStateException("Attempting to poll from dead queue");
+ }
+
+ search_loop:
+ for (int i = 0; i <= max; ++i) {
+ final ArrayDeque queue = queues[i];
+ while ((task = queue.pollFirst()) != null) {
+ if ((referenceCounter = task.trySetCompleting(i)) != null) {
+ break search_loop;
+ }
+ }
+ }
+ }
+
+ if (task == null) {
+ return false;
+ }
+
+ try {
+ task.executeInternal();
+ } finally {
+ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord);
+ }
+
+ return true;
+ }
+
+ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask {
+
+ private static final AtomicLong REFERENCE_COUNTER_NOT_SET = new AtomicLong(-1L);
+
+ private final WorldRegionTaskData world;
+ private final int chunkX;
+ private final int chunkZ;
+ private final long sectionLowerLeftCoord; // chunk coordinate
+ private final boolean isChunkTask;
+
+ private volatile AtomicLong referenceCounter;
+ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", AtomicLong.class);
+ private Runnable run;
+ private volatile PrioritisedExecutor.Priority priority;
+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", PrioritisedExecutor.Priority.class);
+
+ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.isChunkTask = isChunkTask;
+ this.run = run;
+ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET);
+ this.setPriorityPlain(priority);
+
+ final int regionShift = world.world.regioniser.sectionChunkShift;
+ final int regionMask = (1 << regionShift) - 1;
+
+ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask);
+ }
+
+ private PrioritisedExecutor.Priority getPriorityVolatile() {
+ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.getVolatile(this);
+ }
+
+ private void setPriorityPlain(final PrioritisedExecutor.Priority priority) {
+ PRIORITY_HANDLE.set(this, priority);
+ }
+
+ private void setPriorityVolatile(final PrioritisedExecutor.Priority priority) {
+ PRIORITY_HANDLE.setVolatile(this, priority);
+ }
+
+ private PrioritisedExecutor.Priority compareAndExchangePriority(final PrioritisedExecutor.Priority expect, final PrioritisedExecutor.Priority update) {
+ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ private void setReferenceCounterPlain(final AtomicLong value) {
+ REFERENCE_COUNTER_HANDLE.set(this, value);
+ }
+
+ private AtomicLong getReferenceCounterVolatile() {
+ return (AtomicLong)REFERENCE_COUNTER_HANDLE.get(this);
+ }
+
+ private AtomicLong compareAndExchangeReferenceCounter(final AtomicLong expect, final AtomicLong update) {
+ return (AtomicLong)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ private void executeInternal() {
+ try {
+ this.run.run();
+ } finally {
+ this.run = null;
+ }
+ }
+
+ private void cancelInternal() {
+ this.run = null;
+ }
+
+ private boolean tryComplete(final boolean cancel) {
+ int failures = 0;
+ for (AtomicLong curr = this.getReferenceCounterVolatile();;) {
+ if (curr == null) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
+ ++failures;
+ continue;
+ }
+
+ // we have the reference count, we win no matter what.
+ this.setPriorityVolatile(PrioritisedExecutor.Priority.COMPLETING);
+
+ try {
+ if (cancel) {
+ this.cancelInternal();
+ } else {
+ this.executeInternal();
+ }
+ } finally {
+ if (curr != REFERENCE_COUNTER_NOT_SET) {
+ this.world.decrementReference(curr, this.sectionLowerLeftCoord);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ @Override
+ public boolean queue() {
+ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) {
+ return false;
+ }
+
+ final AtomicLong referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord);
+ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) {
+ // we don't expect race conditions here, so it is OK if we have to needlessly reference count
+ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord);
+ return false;
+ }
+
+ boolean synchronise = false;
+ for (;;) {
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
+ // is done
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
+
+ if (queue == null) {
+ if (!synchronise) {
+ // may be incorrectly null when unsynchronised
+ continue;
+ }
+ // may have been cancelled before we got to the queue
+ if (this.getReferenceCounterVolatile() != null) {
+ throw new IllegalStateException("Expected null ref count when queue does not exist");
+ }
+ // the task never could be polled from the queue, so we return false
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
+ // will decrement the reference count
+ return false;
+ }
+
+ synchronise = true;
+
+ final Boolean res = queue.tryPush(this);
+ if (res == null) {
+ // we were cancelled
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
+ // will decrement the reference count
+ return false;
+ }
+
+ if (!res.booleanValue()) {
+ // failed, try again
+ continue;
+ }
+
+ // successfully queued
+ return true;
+ }
+ }
+
+ private AtomicLong trySetCompleting(final int minPriority) {
+ // first, try to set priority to EXECUTING
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
+ if (curr.isLowerPriority(minPriority)) {
+ return null;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, PrioritisedExecutor.Priority.COMPLETING))) {
+ break;
+ } // else: continue
+ }
+
+ for (AtomicLong curr = this.getReferenceCounterVolatile();;) {
+ if (curr == null) {
+ // something acquired before us
+ return null;
+ }
+
+ if (curr == REFERENCE_COUNTER_NOT_SET) {
+ throw new IllegalStateException();
+ }
+
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
+ continue;
+ }
+ return curr;
+ }
+ }
+
+ private void updatePriorityInQueue() {
+ boolean synchronise = false;
+ for (;;) {
+ final AtomicLong referenceCount = this.getReferenceCounterVolatile();
+ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) {
+ // cancelled or not queued
+ return;
+ }
+
+ if (this.getPriorityVolatile() == PrioritisedExecutor.Priority.COMPLETING) {
+ // cancelled
+ return;
+ }
+
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
+ // is done
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
+
+ if (queue == null) {
+ if (!synchronise) {
+ // may be incorrectly null when unsynchronised
+ continue;
+ }
+ // must have been removed
+ return;
+ }
+
+ synchronise = true;
+
+ final Boolean res = queue.tryPush(this);
+ if (res == null) {
+ // we were cancelled
+ return;
+ }
+
+ if (!res.booleanValue()) {
+ // failed, try again
+ continue;
+ }
+
+ // successfully queued
+ return;
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.getPriorityVolatile();
+ }
+
+ @Override
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
+ int failures = 0;
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr.isLowerOrEqualPriority(priority)) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
+ int failures = 0;
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr == priority) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
+ int failures = 0;
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr.isHigherOrEqualPriority(priority)) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public boolean execute() {
+ return this.tryComplete(false);
+ }
+
+ @Override
+ public boolean cancel() {
+ return this.tryComplete(true);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java
new file mode 100644
index 0000000000000000000000000000000000000000..f55c28bbee70e8418dd0555d50c43ddd434d5347
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java
@@ -0,0 +1,665 @@
+package io.papermc.paper.threadedregions;
+
+import com.destroystokyo.paper.util.maplist.ReferenceList;
+import com.destroystokyo.paper.util.misc.PlayerAreaMap;
+import com.destroystokyo.paper.util.misc.PooledLinkedHashSets;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
+import io.papermc.paper.util.CoordinateUtils;
+import io.papermc.paper.util.TickThread;
+import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.CrashReport;
+import net.minecraft.ReportedException;
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.network.protocol.game.ClientboundDisconnectPacket;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.util.VisibleForDebug;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.village.VillageSiege;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.level.BlockEventData;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.NaturalSpawner;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.TickingBlockEntity;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.material.Fluid;
+import net.minecraft.world.level.redstone.CollectingNeighborUpdater;
+import net.minecraft.world.level.redstone.NeighborUpdater;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.ticks.LevelTicks;
+import org.bukkit.craftbukkit.block.CraftBlockState;
+import org.bukkit.craftbukkit.util.UnsafeList;
+import org.slf4j.Logger;
+import javax.annotation.Nullable;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public final class RegionizedWorldData {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() {
+ @Override
+ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) {
+ // connections
+ for (final Connection conn : from.connections) {
+ into.connections.add(conn);
+ }
+ // time
+ final long fromRedstoneTimeOffset = from.redstoneTime - into.redstoneTime;
+ // entities
+ for (final ServerPlayer player : from.localPlayers) {
+ into.localPlayers.add(player);
+ }
+ for (final Entity entity : from.allEntities) {
+ into.allEntities.add(entity);
+ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ }
+ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) {
+ into.entityTickList.add(iterator.next());
+ }
+ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) {
+ into.navigatingMobs.add(iterator.next());
+ }
+ // block ticking
+ into.blockEvents.addAll(from.blockEvents);
+ // ticklists use game time
+ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset);
+ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset);
+
+ // tile entity ticking
+ for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) {
+ into.pendingBlockEntityTickers.add(tileEntityWrapped);
+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
+ if (tileEntity != null) {
+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ }
+ }
+ for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) {
+ into.blockEntityTickers.add(tileEntityWrapped);
+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
+ if (tileEntity != null) {
+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ }
+ }
+
+ // ticking chunks
+ for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) {
+ into.entityTickingChunks.add(iterator.next());
+ }
+ for (final ChunkHolder holder : from.needsChangeBroadcasting) {
+ into.needsChangeBroadcasting.add(holder);
+ }
+ // redstone torches
+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
+ if (into.redstoneUpdateInfos == null) {
+ into.redstoneUpdateInfos = new ArrayDeque<>();
+ }
+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
+ info.offsetTime(fromRedstoneTimeOffset);
+ into.redstoneUpdateInfos.add(info);
+ }
+ }
+ // light chunks being worked on
+ into.chunksBeingWorkedOn.putAll(from.chunksBeingWorkedOn);
+ // mob spawning
+ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick);
+ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick);
+ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick);
+ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) {
+ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay);
+ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay);
+ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance);
+ }
+ }
+
+ @Override
+ public void split(final RegionizedWorldData from, final int chunkToRegionShift,
+ final Long2ReferenceOpenHashMap regionToData,
+ final ReferenceOpenHashSet dataSet) {
+ // connections
+ for (final Connection conn : from.connections) {
+ final ServerPlayer player = conn.getPlayer();
+ final ChunkPos pos = player.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .connections.add(conn);
+ }
+ // entities
+ for (final ServerPlayer player : from.localPlayers) {
+ final ChunkPos pos = player.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .localPlayers.add(player);
+ }
+ for (final Entity entity : from.allEntities) {
+ final ChunkPos pos = entity.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
+ into.allEntities.add(entity);
+ // Note: entityTickList is a subset of allEntities
+ if (from.entityTickList.contains(entity)) {
+ into.entityTickList.add(entity);
+ }
+ // Note: navigatingMobs is a subset of allEntities
+ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) {
+ into.navigatingMobs.add(mob);
+ }
+ }
+ // block ticking
+ for (final BlockEventData blockEventData : from.blockEvents) {
+ final BlockPos pos = blockEventData.pos();
+ final int chunkX = pos.getX() >> 4;
+ final int chunkZ = pos.getZ() >> 4;
+
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
+ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events
+ // is just some list. So if it unloads, I guess it's just lost.
+ if (into != null) {
+ into.blockEvents.add(blockEventData);
+ }
+ }
+
+ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
+ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
+
+ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ReferenceMap.Entry entry = iterator.next();
+ final long key = entry.getLongKey();
+ final RegionizedWorldData worldData = entry.getValue();
+
+ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks);
+ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks);
+ }
+
+ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData);
+ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData);
+
+ // tile entity ticking
+ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) {
+ final BlockPos pos = tileEntity.getPos();
+ final int chunkX = pos.getX() >> 4;
+ final int chunkZ = pos.getZ() >> 4;
+
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
+ if (into != null) {
+ into.pendingBlockEntityTickers.add(tileEntity);
+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
+ // marked as removed. So if there is no section, it's probably removed!
+ }
+ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) {
+ final BlockPos pos = tileEntity.getPos();
+ final int chunkX = pos.getX() >> 4;
+ final int chunkZ = pos.getZ() >> 4;
+
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
+ if (into != null) {
+ into.blockEntityTickers.add(tileEntity);
+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
+ // marked as removed. So if there is no section, it's probably removed!
+ }
+ // time
+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
+ regionizedWorldData.redstoneTime = from.redstoneTime;
+ }
+ // ticking chunks
+ for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) {
+ final LevelChunk levelChunk = iterator.next();
+ final ChunkPos pos = levelChunk.getPos();
+
+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .entityTickingChunks.add(levelChunk);
+ }
+
+ for (final ChunkHolder holder : from.needsChangeBroadcasting) {
+ final ChunkPos pos = holder.pos;
+
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .needsChangeBroadcasting.add(holder);
+ }
+ // redstone torches
+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
+ final BlockPos pos = info.pos;
+
+ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift));
+ if (worldData != null) {
+ if (worldData.redstoneUpdateInfos == null) {
+ worldData.redstoneUpdateInfos = new ArrayDeque<>();
+ }
+ worldData.redstoneUpdateInfos.add(info);
+ } // else: chunk unloaded
+ }
+ }
+ // light chunks being worked on
+ for (final Iterator iterator = from.chunksBeingWorkedOn.long2IntEntrySet().fastIterator(); iterator.hasNext();) {
+ final Long2IntOpenHashMap.Entry entry = iterator.next();
+ final long pos = entry.getLongKey();
+ final int chunkX = CoordinateUtils.getChunkX(pos);
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
+ final int value = entry.getIntValue();
+
+ // should never be null, as it is a reference counter for ticket
+ regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)).chunksBeingWorkedOn.put(pos, value);
+ }
+ // mob spawning
+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
+ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick;
+ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick;
+ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick;
+ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay;
+ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance;
+ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay;
+ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid
+ }
+ }
+ };
+
+ public final ServerLevel world;
+
+ private RegionizedServer.WorldLevelData tickData;
+
+ // connections
+ public final List connections = new ArrayList<>();
+
+ // misc. fields
+ private boolean isHandlingTick;
+
+ public void setHandlingTick(final boolean to) {
+ this.isHandlingTick = to;
+ }
+
+ public boolean isHandlingTick() {
+ return this.isHandlingTick;
+ }
+
+ // entities
+ private final List localPlayers = new ArrayList<>();
+ private final ReferenceList allEntities = new ReferenceList<>();
+ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>();
+ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>();
+
+ // block ticking
+ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>();
+ private final LevelTicks blockLevelTicks;
+ private final LevelTicks fluidLevelTicks;
+
+ // tile entity ticking
+ private final List pendingBlockEntityTickers = new ArrayList<>();
+ private final List blockEntityTickers = new ArrayList<>();
+ private boolean tickingBlockEntities;
+
+ // time
+ private long redstoneTime = 1L;
+
+ public long getRedstoneGameTime() {
+ return this.redstoneTime;
+ }
+
+ public void setRedstoneGameTime(final long to) {
+ this.redstoneTime = to;
+ }
+
+ // ticking chunks
+ private final IteratorSafeOrderedReferenceSet entityTickingChunks = new IteratorSafeOrderedReferenceSet<>();
+ private final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>();
+
+ // Paper/CB api hook misc
+ // don't bother to merge/split these, no point
+ // From ServerLevel
+ public boolean hasPhysicsEvent = true; // Paper
+ public boolean hasEntityMoveEvent = false; // Paper
+ // Paper start - Optimize Hoppers
+ public boolean skipPullModeEventFire = false;
+ public boolean skipPushModeEventFire = false;
+ public boolean skipHopperEvents = false;
+ // Paper end - Optimize Hoppers
+ public long lastMidTickExecuteFailure;
+ public long lastMidTickExecute;
+ // From Level
+ public boolean populating;
+ public final NeighborUpdater neighborUpdater;
+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
+ public boolean captureBlockStates = false;
+ public boolean captureTreeGeneration = false;
+ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
+ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
+ public List captureDrops;
+ // Paper start
+ public int wakeupInactiveRemainingAnimals;
+ public int wakeupInactiveRemainingFlying;
+ public int wakeupInactiveRemainingMonsters;
+ public int wakeupInactiveRemainingVillagers;
+ // Paper end
+ public final TempCollisionList tempCollisionList = new TempCollisionList<>();
+ public final TempCollisionList tempEntitiesList = new TempCollisionList<>();
+ public int currentPrimedTnt = 0; // Spigot
+ @Nullable
+ @VisibleForDebug
+ public NaturalSpawner.SpawnState lastSpawnState;
+ public boolean shouldSignal = true;
+
+ // not transient
+ public java.util.ArrayDeque redstoneUpdateInfos;
+ public final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
+
+ public static final class TempCollisionList {
+ final UnsafeList list = new UnsafeList<>(64);
+ boolean inUse;
+
+ public UnsafeList get() {
+ if (this.inUse) {
+ return new UnsafeList<>(16);
+ }
+ this.inUse = true;
+ return this.list;
+ }
+
+ public void ret(List list) {
+ if (list != this.list) {
+ return;
+ }
+
+ ((UnsafeList)list).setSize(0);
+ this.inUse = false;
+ }
+
+ public void reset() {
+ this.list.completeReset();
+ }
+ }
+ public void resetCollisionLists() {
+ this.tempCollisionList.reset();
+ this.tempEntitiesList.reset();
+ }
+
+ // Mob spawning
+ private final PooledLinkedHashSets pooledHashSets = new PooledLinkedHashSets<>();
+ public final PlayerAreaMap mobSpawnMap = new PlayerAreaMap(this.pooledHashSets);
+ public int catSpawnerNextTick = 0;
+ public int patrolSpawnerNextTick = 0;
+ public int phantomSpawnerNextTick = 0;
+ public int wanderingTraderTickDelay = Integer.MIN_VALUE;
+ public int wanderingTraderSpawnDelay;
+ public int wanderingTraderSpawnChance;
+ public VillageSiegeState villageSiegeState = new VillageSiegeState();
+
+ public static final class VillageSiegeState {
+ public boolean hasSetupSiege;
+ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
+ public int zombiesToSpawn;
+ public int nextSpawnTime;
+ public int spawnX;
+ public int spawnY;
+ public int spawnZ;
+ }
+
+ public RegionizedWorldData(final ServerLevel world) {
+ this.world = world;
+ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, true);
+ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, false);
+ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax);
+
+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
+ this.updateTickData();
+ }
+
+ public RegionizedServer.WorldLevelData getTickData() {
+ return this.tickData;
+ }
+
+ public void updateTickData() {
+ this.tickData = this.world.tickData;
+ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
+ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
+ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
+ }
+
+ // connections
+ public void tickConnections() {
+ final List connections = new ArrayList<>(this.connections);
+ Collections.shuffle(connections);
+ for (final Connection conn : connections) {
+ if (!conn.isConnected()) {
+ conn.handleDisconnection();
+ this.connections.remove(conn);
+ // note: ALL connections HERE have a player
+ final ServerPlayer player = conn.getPlayer();
+ // now that the connection is removed, we can allow this region to die
+ player.getLevel().chunkSource.removeTicketAtLevel(
+ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos,
+ ChunkHolderManager.MAX_TICKET_LEVEL,
+ player.connection.disconnectTicketId
+ );
+ continue;
+ }
+ if (!this.connections.contains(conn)) {
+ // removed by connection tick?
+ continue;
+ }
+
+ try {
+ conn.tick();
+ } catch (final Exception exception) {
+ if (conn.isMemoryConnection()) {
+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
+ }
+
+ LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper
+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
+
+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
+ conn.disconnect(ichatmutablecomponent);
+ }));
+ conn.setReadOnly();
+ continue;
+ }
+ }
+ }
+
+ // entities hooks
+ public Iterable getLocalEntities() {
+ return this.allEntities;
+ }
+
+ public Entity[] getLocalEntitiesCopy() {
+ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class);
+ }
+
+ public List getLocalPlayers() {
+ return this.localPlayers;
+ }
+
+ public void addEntityTickingEntity(final Entity entity) {
+ if (!TickThread.isTickThreadFor(entity)) {
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
+ }
+ this.entityTickList.add(entity);
+ }
+
+ public boolean hasEntityTickingEntity(final Entity entity) {
+ return this.entityTickList.contains(entity);
+ }
+
+ public void removeEntityTickingEntity(final Entity entity) {
+ if (!TickThread.isTickThreadFor(entity)) {
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
+ }
+ this.entityTickList.remove(entity);
+ }
+
+ public void forEachTickingEntity(final Consumer action) {
+ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator();
+ try {
+ while (iterator.hasNext()) {
+ action.accept(iterator.next());
+ }
+ } finally {
+ iterator.finishedIterating();
+ }
+ }
+
+ public void addEntity(final Entity entity) {
+ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) {
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
+ }
+ if (this.allEntities.add(entity)) {
+ if (entity instanceof ServerPlayer player) {
+ this.localPlayers.add(player);
+ }
+ }
+ }
+
+ public boolean hasEntity(final Entity entity) {
+ return this.allEntities.contains(entity);
+ }
+
+ public void removeEntity(final Entity entity) {
+ if (!TickThread.isTickThreadFor(entity)) {
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
+ }
+ if (this.allEntities.remove(entity)) {
+ if (entity instanceof ServerPlayer player) {
+ this.localPlayers.remove(player);
+ }
+ }
+ }
+
+ public void addNavigatingMob(final Mob mob) {
+ if (!TickThread.isTickThreadFor(mob)) {
+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
+ }
+ this.navigatingMobs.add(mob);
+ }
+
+ public void removeNavigatingMob(final Mob mob) {
+ if (!TickThread.isTickThreadFor(mob)) {
+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
+ }
+ this.navigatingMobs.remove(mob);
+ }
+
+ public Iterator getNavigatingMobs() {
+ return this.navigatingMobs.unsafeIterator();
+ }
+
+ // block ticking hooks
+ // Since block event data does not require chunk holders to be created for the chunk they reside in,
+ // it's not actually guaranteed that when merging / splitting data that we actually own the data...
+ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to
+ // make the code easier by simply discarding it in such an event
+ public void pushBlockEvent(final BlockEventData blockEventData) {
+ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async");
+ this.blockEvents.add(blockEventData);
+ }
+
+ public void pushBlockEvents(final Collection extends BlockEventData> blockEvents) {
+ for (final BlockEventData blockEventData : blockEvents) {
+ this.pushBlockEvent(blockEventData);
+ }
+ }
+
+ public void removeIfBlockEvents(final Predicate super BlockEventData> predicate) {
+ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) {
+ final BlockEventData blockEventData = iterator.next();
+ if (predicate.test(blockEventData)) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public BlockEventData removeFirstBlockEvent() {
+ BlockEventData ret;
+ while (!this.blockEvents.isEmpty()) {
+ ret = this.blockEvents.removeFirst();
+ if (TickThread.isTickThreadFor(this.world, ret.pos())) {
+ return ret;
+ } // else: chunk must have been unloaded
+ }
+
+ return null;
+ }
+
+ public LevelTicks getBlockLevelTicks() {
+ return this.blockLevelTicks;
+ }
+
+ public LevelTicks getFluidLevelTicks() {
+ return this.fluidLevelTicks;
+ }
+
+ // tile entity ticking
+ public void addBlockEntityTicker(final TickingBlockEntity ticker) {
+ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region");
+
+ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker);
+ }
+
+ public void seTtickingBlockEntities(final boolean to) {
+ this.tickingBlockEntities = true;
+ }
+
+ public List getBlockEntityTickers() {
+ return this.blockEntityTickers;
+ }
+
+ public void pushPendingTickingBlockEntities() {
+ if (!this.pendingBlockEntityTickers.isEmpty()) {
+ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
+ this.pendingBlockEntityTickers.clear();
+ }
+ }
+
+ // ticking chunks
+ public void addEntityTickingChunks(final LevelChunk levelChunk) {
+ this.entityTickingChunks.add(levelChunk);
+ }
+
+ public void removeEntityTickingChunk(final LevelChunk levelChunk) {
+ this.entityTickingChunks.remove(levelChunk);
+ }
+
+ public IteratorSafeOrderedReferenceSet getEntityTickingChunks() {
+ return this.entityTickingChunks;
+ }
+
+ public void addChunkHolderNeedsBroadcasting(final ChunkHolder holder) {
+ this.needsChangeBroadcasting.add(holder);
+ }
+
+ public void removeChunkHolderNeedsBroadcasting(final ChunkHolder holder) {
+ this.needsChangeBroadcasting.remove(holder);
+ }
+
+ public ReferenceOpenHashSet getNeedsChangeBroadcasting() {
+ return this.needsChangeBroadcasting;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java
new file mode 100644
index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java
@@ -0,0 +1,91 @@
+package io.papermc.paper.threadedregions;
+
+/**
+ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest.
+ */
+public final class Schedule {
+
+ private long lastPeriod;
+
+ /**
+ * Initialises a schedule with the provided period.
+ * @param firstPeriod The last time an event of interest occurred.
+ * @see #setLastPeriod(long)
+ */
+ public Schedule(final long firstPeriod) {
+ this.lastPeriod = firstPeriod;
+ }
+
+ /**
+ * Updates the last period to the specified value. This call sets the last "time" the event
+ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is
+ * the provided time plus the period length provided to {@code getDeadline}.
+ * @param value The value to set the last period to.
+ */
+ public void setLastPeriod(final long value) {
+ this.lastPeriod = value;
+ }
+
+ /**
+ * Returns the last time the event of interest should have taken place.
+ */
+ public long getLastPeriod() {
+ return this.lastPeriod;
+ }
+
+ /**
+ * Returns the number of times the event of interest should have taken place between the last
+ * period and the provided time given the period between each event.
+ * @param periodLength The length of the period between events in ns.
+ * @param time The provided time.
+ */
+ public int getPeriodsAhead(final long periodLength, final long time) {
+ final long difference = time - this.lastPeriod;
+ final int ret = (int)(Math.abs(difference) / periodLength);
+ return difference >= 0 ? ret : -ret;
+ }
+
+ /**
+ * Returns the next starting deadline for the event of interest to take place,
+ * given the provided period length.
+ * @param periodLength The provided period length.
+ */
+ public long getDeadline(final long periodLength) {
+ return this.lastPeriod + periodLength;
+ }
+
+ /**
+ * Adjusts the last period so that the next starting deadline returned is the next period specified,
+ * given the provided period length.
+ * @param nextPeriod The specified next starting deadline.
+ * @param periodLength The specified period length.
+ */
+ public void setNextPeriod(final long nextPeriod, final long periodLength) {
+ this.lastPeriod = nextPeriod - periodLength;
+ }
+
+ /**
+ * Increases the last period by the specified number of periods and period length.
+ * The specified number of periods may be < 0, in which case the last period
+ * will decrease.
+ * @param periods The specified number of periods.
+ * @param periodLength The specified period length.
+ */
+ public void advanceBy(final int periods, final long periodLength) {
+ this.lastPeriod += (long)periods * periodLength;
+ }
+
+ /**
+ * Sets the last period so that it is the specified number of periods ahead
+ * given the specified time and period length.
+ * @param periodsToBeAhead Specified number of periods to be ahead by.
+ * @param periodLength The specified period length.
+ * @param time The specified time.
+ */
+ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) {
+ final int periodsAhead = this.getPeriodsAhead(periodLength, time);
+ final int periodsToAdd = periodsToBeAhead - periodsAhead;
+
+ this.lastPeriod -= (long)periodsToAdd * periodLength;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..84b4ff07735fb84e28ee8966ffdedb1bb3d07dff
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.completable.Completable;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import java.util.function.Consumer;
+
+public final class TeleportUtils {
+
+ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) {
+ // retrieve coordinates
+ final Completable positionCompletable = new Completable<>();
+
+ positionCompletable.addWaiter(
+ (final Location loc, final Throwable thr) -> {
+ if (loc == null) {
+ onComplete.accept(null);
+ return;
+ }
+ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule(
+ (final Entity realFrom) -> {
+ final Vec3 pos = new Vec3(
+ loc.getX(), loc.getY(), loc.getZ()
+ );
+ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync(
+ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null,
+ cause, teleportFlags, onComplete
+ );
+ },
+ (final Entity retired) -> {
+ onComplete.accept(null);
+ },
+ 1L
+ );
+ if (!scheduled) {
+ onComplete.accept(null);
+ }
+ }
+ );
+
+ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule(
+ (final Entity target) -> {
+ positionCompletable.complete(target.getBukkitEntity().getLocation());
+ },
+ (final Entity retired) -> {
+ onComplete.accept(null);
+ },
+ 1L
+ );
+ if (!scheduled) {
+ onComplete.accept(null);
+ }
+ }
+
+ private TeleportUtils() {}
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff290d9dcd3494215ce7e41945ef47f00e8c9489
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java
@@ -0,0 +1,1211 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import com.destroystokyo.paper.util.SneakyThrow;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.StampedLock;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public final int regionSectionChunkSize;
+ public final int sectionChunkShift;
+ public final int minSectionRecalcCount;
+ public final int emptySectionCreateRadius;
+ public final int regionSectionMergeRadius;
+ public final double maxDeadRegionPercent;
+ public final ServerLevel world;
+
+ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>();
+ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>();
+ private final RegionCallbacks callbacks;
+ private final StampedLock regionLock = new StampedLock();
+ private Thread writeLockOwner;
+
+ /*
+ static final record Operation(String type, int chunkX, int chunkZ) {}
+ private final MultiThreadedQueue ops = new MultiThreadedQueue<>();
+ */
+
+ /*
+ * See REGION_LOGIC.md for complete details on what this class is doing
+ */
+
+ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent,
+ final int emptySectionCreateRadius, final int regionSectionMergeRadius,
+ final int regionSectionChunkShift, final ServerLevel world,
+ final RegionCallbacks callbacks) {
+ if (emptySectionCreateRadius <= 0) {
+ throw new IllegalStateException("Region section create radius must be > 0");
+ }
+ if (regionSectionMergeRadius <= 0) {
+ throw new IllegalStateException("Region section merge radius must be > 0");
+ }
+ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
+ this.sectionChunkShift = regionSectionChunkShift;
+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
+ this.maxDeadRegionPercent = maxDeadRegionPercent;
+ this.emptySectionCreateRadius = emptySectionCreateRadius;
+ this.regionSectionMergeRadius = regionSectionMergeRadius;
+ this.world = world;
+ this.callbacks = callbacks;
+ //this.loadTestData();
+ }
+
+ /*
+ private static String substr(String val, String prefix, int from) {
+ int idx = val.indexOf(prefix, from) + prefix.length();
+ int idx2 = val.indexOf(',', idx);
+ if (idx2 == -1) {
+ idx2 = val.indexOf(']', idx);
+ }
+ return val.substring(idx, idx2);
+ }
+
+ private void loadTestData() {
+ if (true) {
+ return;
+ }
+ try {
+ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray();
+
+ List ops = new ArrayList<>();
+
+ for (JsonElement elem : arr) {
+ JsonObject obj = elem.getAsJsonObject();
+ String val = obj.get("value").getAsString();
+
+ String type = substr(val, "type=", 0);
+ String x = substr(val, "chunkX=", 0);
+ String z = substr(val, "chunkZ=", 0);
+
+ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z)));
+ }
+
+ for (Operation op : ops) {
+ switch (op.type) {
+ case "add": {
+ this.addChunk(op.chunkX, op.chunkZ);
+ break;
+ }
+ case "remove": {
+ this.removeChunk(op.chunkX, op.chunkZ);
+ break;
+ }
+ case "mark_ticking": {
+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking();
+ break;
+ }
+ case "rel_region": {
+ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) {
+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking();
+ }
+ break;
+ }
+ }
+ }
+
+ } catch (final Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+ */
+
+ public void acquireReadLock() {
+ this.regionLock.readLock();
+ }
+
+ public void releaseReadLock() {
+ this.regionLock.tryUnlockRead();
+ }
+
+ private void acquireWriteLock() {
+ final Thread currentThread = Thread.currentThread();
+ if (this.writeLockOwner == currentThread) {
+ throw new IllegalStateException("Cannot recursively operate in the regioniser");
+ }
+ this.regionLock.writeLock();
+ this.writeLockOwner = currentThread;
+ }
+
+ private void releaseWriteLock() {
+ this.writeLockOwner = null;
+ this.regionLock.tryUnlockWrite();
+ }
+
+ private void onRegionCreate(final ThreadedRegion region) {
+ final ThreadedRegion conflict;
+ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) {
+ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict);
+ }
+ }
+
+ private void onRegionDestroy(final ThreadedRegion region) {
+ final ThreadedRegion removed = this.regionsById.remove(region.id);
+ if (removed != region) {
+ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed);
+ }
+ }
+
+ public int getSectionCoordinate(final int chunkCoordinate) {
+ return chunkCoordinate >> this.sectionChunkShift;
+ }
+
+ public long getSectionKey(final BlockPos pos) {
+ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift);
+ }
+
+ public long getSectionKey(final ChunkPos pos) {
+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
+ }
+
+ public long getSectionKey(final Entity entity) {
+ final ChunkPos pos = entity.chunkPosition();
+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
+ }
+
+ public void computeForAllRegions(final Consumer super ThreadedRegion> consumer) {
+ this.regionLock.readLock();
+ try {
+ this.regionsById.forEachValue(consumer);
+ } finally {
+ this.regionLock.tryUnlockRead();
+ }
+ }
+
+ public void computeForAllRegionsUnsynchronised(final Consumer super ThreadedRegion> consumer) {
+ this.regionsById.forEachValue(consumer);
+ }
+
+ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) {
+ final int sectionX = chunkX >> this.sectionChunkShift;
+ final int sectionZ = chunkZ >> this.sectionChunkShift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+
+ final ThreadedRegionSection section = this.sections.get(sectionKey);
+
+ return section == null ? null : section.getRegion();
+ }
+
+ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) {
+ final int sectionX = chunkX >> this.sectionChunkShift;
+ final int sectionZ = chunkZ >> this.sectionChunkShift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+
+ // try an optimistic read
+ {
+ final long readAttempt = this.regionLock.tryOptimisticRead();
+ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey);
+ final ThreadedRegion optimisticRet =
+ optimisticSection == null ? null : optimisticSection.getRegionPlain();
+ if (this.regionLock.validate(readAttempt)) {
+ return optimisticRet;
+ }
+ }
+
+ // failed, fall back to acquiring the lock
+ this.regionLock.readLock();
+ try {
+ final ThreadedRegionSection section = this.sections.get(sectionKey);
+
+ return section == null ? null : section.getRegionPlain();
+ } finally {
+ this.regionLock.tryUnlockRead();
+ }
+ }
+
+ /**
+ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless
+ * addChunk has not been called for it or removeChunk has been previously called.
+ *
+ *
+ * Note that it is illegal to additionally call addChunk or removeChunk for the same
+ * region section in parallel.
+ *
+ */
+ public void addChunk(final int chunkX, final int chunkZ) {
+ final int sectionX = chunkX >> this.sectionChunkShift;
+ final int sectionZ = chunkZ >> this.sectionChunkShift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+
+ // Given that for each section, no addChunk/removeChunk can occur in parallel,
+ // we can avoid the lock IF the section exists AND it has a non-zero chunk count.
+ {
+ final ThreadedRegionSection existing = this.sections.get(sectionKey);
+ if (existing != null && !existing.isEmpty()) {
+ existing.addChunk(chunkX, chunkZ);
+ return;
+ } // else: just acquire the write lock
+ }
+
+ this.acquireWriteLock();
+ try {
+ ThreadedRegionSection section = this.sections.get(sectionKey);
+
+ List> newSections = new ArrayList<>();
+
+ if (section == null) {
+ // no section at all
+ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ);
+ this.sections.put(sectionKey, section);
+ newSections.add(section);
+ } else {
+ section.addChunk(chunkX, chunkZ);
+ }
+ // due to the fast check from above, we know the section is empty whether we needed to create it or not
+
+ // enforce the adjacency invariant by creating / updating neighbour sections
+ final int createRadius = this.emptySectionCreateRadius;
+ final int searchRadius = createRadius + this.regionSectionMergeRadius;
+ ReferenceOpenHashSet> nearbyRegions = null;
+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) {
+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
+ final boolean inCreateRange = squareDistance <= createRadius;
+
+ final int neighbourX = dx + sectionX;
+ final int neighbourZ = dz + sectionZ;
+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey);
+
+ if (neighbourSection != null) {
+ if (nearbyRegions == null) {
+ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1);
+ }
+ nearbyRegions.add(neighbourSection.getRegionPlain());
+ }
+
+ if (!inCreateRange) {
+ continue;
+ }
+
+ // we need to ensure the section exists
+ if (neighbourSection != null) {
+ // nothing else to do
+ neighbourSection.incrementNonEmptyNeighbours();
+ continue;
+ }
+ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1);
+ if (null != this.sections.put(neighbourKey, neighbourSection)) {
+ throw new IllegalStateException("Failed to insert new section");
+ }
+ newSections.add(neighbourSection);
+ }
+ }
+
+ final ThreadedRegion regionOfInterest;
+ final boolean regionOfInterestAlive;
+ if (nearbyRegions == null) {
+ // we can simply create a new region, don't have neighbours to worry about merging into
+ regionOfInterest = new ThreadedRegion<>(this);
+ regionOfInterestAlive = true;
+
+ for (int i = 0, len = newSections.size(); i < len; ++i) {
+ regionOfInterest.addSection(newSections.get(i));
+ }
+
+ // only call create callback after adding sections
+ regionOfInterest.onCreate();
+ } else {
+ // need to merge the regions
+
+ ThreadedRegion firstUnlockedRegion = null;
+
+ for (final ThreadedRegion region : nearbyRegions) {
+ if (region.isTicking()) {
+ continue;
+ }
+ firstUnlockedRegion = region;
+ break;
+ }
+
+ if (firstUnlockedRegion != null) {
+ regionOfInterest = firstUnlockedRegion;
+ } else {
+ regionOfInterest = new ThreadedRegion<>(this);
+ }
+
+ for (int i = 0, len = newSections.size(); i < len; ++i) {
+ regionOfInterest.addSection(newSections.get(i));
+ }
+
+ // only call create callback after adding sections
+ if (firstUnlockedRegion == null) {
+ regionOfInterest.onCreate();
+ }
+
+ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) {
+ // nothing to do further, no need to merge anything
+ return;
+ }
+
+ // we need to now tell all the other regions to merge into the region we just created,
+ // and to merge all the ones we can immediately
+
+ boolean delayedTrueMerge = false;
+
+ for (final ThreadedRegion region : nearbyRegions) {
+ if (region == regionOfInterest) {
+ continue;
+ }
+ // need the relaxed check, as the region may already be
+ // a merge target
+ if (!region.tryKill()) {
+ regionOfInterest.mergeIntoLater(region);
+ delayedTrueMerge = true;
+ } else {
+ region.mergeInto(regionOfInterest);
+ }
+ }
+
+ if (delayedTrueMerge && firstUnlockedRegion != null) {
+ // we need to retire this region, as it can no longer tick
+ if (regionOfInterest.state == ThreadedRegion.STATE_READY) {
+ regionOfInterest.state = ThreadedRegion.STATE_TRANSIENT;
+ this.callbacks.onRegionInactive(regionOfInterest);
+ }
+ }
+
+ // need to set alive if we created it and we didn't delay a merge
+ regionOfInterestAlive = firstUnlockedRegion == null && !delayedTrueMerge && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty();
+ }
+
+ if (regionOfInterestAlive) {
+ regionOfInterest.state = ThreadedRegion.STATE_READY;
+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) {
+ throw new IllegalStateException("Should not happen on region " + this);
+ }
+ this.callbacks.onRegionActive(regionOfInterest);
+ }
+ } catch (final Throwable throwable) {
+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
+ SneakyThrow.sneaky(throwable);
+ return; // unreachable
+ } finally {
+ this.releaseWriteLock();
+ }
+ }
+
+ public void removeChunk(final int chunkX, final int chunkZ) {
+ final int sectionX = chunkX >> this.sectionChunkShift;
+ final int sectionZ = chunkZ >> this.sectionChunkShift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+
+ // Given that for each section, no addChunk/removeChunk can occur in parallel,
+ // we can avoid the lock IF the section exists AND it has a chunk count > 1
+ final ThreadedRegionSection section = this.sections.get(sectionKey);
+ if (section == null) {
+ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section");
+ }
+ if (!section.hasOnlyOneChunk()) {
+ // chunk will not go empty, so we don't need to acquire the lock
+ section.removeChunk(chunkX, chunkZ);
+ return;
+ }
+
+ this.acquireWriteLock();
+ try {
+ section.removeChunk(chunkX, chunkZ);
+
+ final int searchRadius = this.emptySectionCreateRadius;
+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) {
+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final int neighbourX = dx + sectionX;
+ final int neighbourZ = dz + sectionZ;
+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey);
+
+ // should be non-null here always
+ neighbourSection.decrementNonEmptyNeighbours();
+ }
+ }
+ } catch (final Throwable throwable) {
+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
+ SneakyThrow.sneaky(throwable);
+ return; // unreachable
+ } finally {
+ this.releaseWriteLock();
+ }
+ }
+
+ // must hold regionLock
+ private void onRegionRelease(final ThreadedRegion region) {
+ if (!region.mergeIntoLater.isEmpty()) {
+ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!");
+ }
+
+ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty();
+
+ // is this region supposed to merge into any other region?
+ if (hasExpectingMerges) {
+ // merge the regions into this one
+ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone();
+ for (final ThreadedRegion mergeFrom : expectingMergeFrom) {
+ if (!mergeFrom.tryKill()) {
+ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region);
+ }
+ mergeFrom.mergeInto(region);
+ }
+
+ if (!region.expectingMergeFrom.isEmpty()) {
+ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom);
+ }
+
+ if (!region.mergeIntoLater.isEmpty()) {
+ // There is another nearby ticking region that we need to merge into
+ region.state = ThreadedRegion.STATE_TRANSIENT;
+ this.callbacks.onRegionInactive(region);
+ // return to avoid removing dead sections or splitting, these actions will be performed
+ // by the region we merge into
+ return;
+ }
+ }
+
+ // now check whether we need to recalculate regions
+ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections()
+ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent);
+ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty();
+ if (removeDeadSections) {
+ // kill dead sections
+ for (final ThreadedRegionSection deadSection : region.deadSections) {
+ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ);
+
+ if (!deadSection.isEmpty()) {
+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
+ }
+ if (deadSection.hasNonEmptyNeighbours()) {
+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!");
+ }
+ if (!region.sectionByKey.remove(key, deadSection)) {
+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
+ }
+ if (this.sections.remove(key) != deadSection) {
+ throw new IllegalStateException("Cannot remove dead section '" +
+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key));
+ }
+ }
+ region.deadSections.clear();
+ }
+
+ // if we removed dead sections, we should check if the region can be split into smaller ones
+ // otherwise, the region remains alive
+ if (!removedDeadSections) {
+ // didn't remove dead sections, don't check for split
+ region.state = ThreadedRegion.STATE_READY;
+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
+ throw new IllegalStateException("Illegal state " + region);
+ }
+ return;
+ }
+
+ // first, we need to build copy of coordinate->section map of all sections in recalculate
+ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone();
+
+ if (recalculateSections.isEmpty()) {
+ // looks like the region's sections were all dead, and now there is no region at all
+ region.state = ThreadedRegion.STATE_DEAD;
+ region.onRemove(true);
+ return;
+ }
+
+ // merge radius is max, since recalculateSections includes the dead or empty sections
+ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius);
+
+ final List>> newRegions = new ArrayList<>();
+ while (!recalculateSections.isEmpty()) {
+ // select any section, then BFS around it to find all of its neighbours to form a region
+ // once no more neighbours are found, the region is complete
+ final List> currRegion = new ArrayList<>();
+ final Iterator> firstIterator = recalculateSections.values().iterator();
+
+ currRegion.add(firstIterator.next());
+ firstIterator.remove();
+ search_loop:
+ for (int idx = 0; idx < currRegion.size(); ++idx) {
+ final ThreadedRegionSection curr = currRegion.get(idx);
+ final int centerX = curr.sectionX;
+ final int centerZ = curr.sectionZ;
+
+ // find neighbours in radius
+ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) {
+ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ));
+ if (section == null) {
+ continue;
+ }
+
+ currRegion.add(section);
+
+ if (recalculateSections.isEmpty()) {
+ // no point in searching further
+ break search_loop;
+ }
+ }
+ }
+ }
+
+ newRegions.add(currRegion);
+ }
+
+ // now we have split the regions into separate parts, we can split recalculate
+
+ if (newRegions.size() == 1) {
+ // no need to split anything, we're done here
+ region.state = ThreadedRegion.STATE_READY;
+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
+ throw new IllegalStateException("Illegal state " + region);
+ }
+ return;
+ }
+
+ // need to split the region, so we need to kill the old one first
+ region.state = ThreadedRegion.STATE_DEAD;
+ region.onRemove(true);
+
+ // create new regions
+ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>();
+ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>();
+
+ for (final List> sections : newRegions) {
+ final ThreadedRegion newRegion = new ThreadedRegion<>(this);
+ newRegionsSet.add(newRegion);
+
+ for (final ThreadedRegionSection section : sections) {
+ section.setRegionRelease(null);
+ newRegion.addSection(section);
+ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion);
+ if (curr != null) {
+ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion);
+ }
+ }
+ }
+
+ region.split(newRegionsMap, newRegionsSet);
+
+ // only after invoking data callbacks
+
+ for (final ThreadedRegion newRegion : newRegionsSet) {
+ newRegion.state = ThreadedRegion.STATE_READY;
+ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) {
+ throw new IllegalStateException("Illegal state " + newRegion);
+ }
+ newRegion.onCreate();
+ this.callbacks.onRegionActive(newRegion);
+ }
+ }
+
+ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> {
+
+ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong();
+
+ private static final int STATE_TRANSIENT = 0;
+ private static final int STATE_READY = 1;
+ private static final int STATE_TICKING = 2;
+ private static final int STATE_DEAD = 3;
+
+ public final long id;
+
+ private int state;
+
+ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>();
+ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>();
+
+ public final ThreadedRegionizer regioniser;
+
+ private final R data;
+
+ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>();
+ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>();
+
+ public ThreadedRegion(final ThreadedRegionizer regioniser) {
+ this.regioniser = regioniser;
+ this.id = REGION_ID_GENERATOR.getAndIncrement();
+ this.state = STATE_TRANSIENT;
+ this.data = regioniser.callbacks.createNewData(this);
+ }
+
+ public LongArrayList getOwnedSections() {
+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread();
+ if (lock) {
+ this.regioniser.regionLock.readLock();
+ }
+ try {
+ final LongArrayList ret = new LongArrayList(this.sectionByKey.size());
+ ret.addAll(this.sectionByKey.keySet());
+
+ return ret;
+ } finally {
+ if (lock) {
+ this.regioniser.regionLock.tryUnlockRead();
+ }
+ }
+ }
+
+ public ChunkPos getCenterChunk() {
+ final LongArrayList sections = this.getOwnedSections();
+
+ sections.sort(null);
+
+ // note: regions always have at least one section
+ final long middle = sections.getLong(sections.size() >> 1);
+
+ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle));
+ }
+
+ private void onCreate() {
+ this.regioniser.onRegionCreate(this);
+ this.regioniser.callbacks.onRegionCreate(this);
+ }
+
+ private void onRemove(final boolean wasActive) {
+ if (wasActive) {
+ this.regioniser.callbacks.onRegionInactive(this);
+ }
+ this.regioniser.callbacks.onRegionDestroy(this);
+ this.regioniser.onRegionDestroy(this);
+ }
+
+ private final boolean hasNoAliveSections() {
+ return this.deadSections.size() == this.sectionByKey.size();
+ }
+
+ private final double getDeadSectionPercent() {
+ return (double)this.deadSections.size() / (double)this.sectionByKey.size();
+ }
+
+ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) {
+ if (this.data != null) {
+ this.data.split(this.regioniser, into, regions);
+ }
+ }
+
+ private void mergeInto(final ThreadedRegion mergeTarget) {
+ if (this == mergeTarget) {
+ throw new IllegalStateException("Cannot merge a region onto itself");
+ }
+ if (!this.isDead()) {
+ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget);
+ } else if (mergeTarget.isDead()) {
+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget);
+ }
+
+ for (final ThreadedRegionSection section : this.sectionByKey.values()) {
+ section.setRegionRelease(null);
+ mergeTarget.addSection(section);
+ }
+ for (final ThreadedRegionSection deadSection : this.deadSections) {
+ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) {
+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this);
+ }
+ if (!mergeTarget.deadSections.add(deadSection)) {
+ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this);
+ }
+ }
+
+ // forward merge expectations
+ for (final ThreadedRegion region : this.expectingMergeFrom) {
+ if (!region.mergeIntoLater.remove(this)) {
+ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?");
+ }
+ if (region != mergeTarget) {
+ region.mergeIntoLater(mergeTarget);
+ }
+ }
+
+ // forward merge into
+ for (final ThreadedRegion region : this.mergeIntoLater) {
+ if (!region.expectingMergeFrom.remove(this)) {
+ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?");
+ }
+ if (region != mergeTarget) {
+ mergeTarget.mergeIntoLater(region);
+ }
+ }
+
+ // finally, merge data
+ if (this.data != null) {
+ this.data.mergeInto(mergeTarget);
+ }
+ }
+
+ private void mergeIntoLater(final ThreadedRegion region) {
+ if (region.isDead()) {
+ throw new IllegalStateException("Trying to merge later into a dead region: " + region);
+ }
+ final boolean add1, add2;
+ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) {
+ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2);
+ }
+ }
+
+ private boolean tryKill() {
+ switch (this.state) {
+ case STATE_TRANSIENT: {
+ this.state = STATE_DEAD;
+ this.onRemove(false);
+ return true;
+ }
+ case STATE_READY: {
+ this.state = STATE_DEAD;
+ this.onRemove(true);
+ return true;
+ }
+ case STATE_TICKING: {
+ return false;
+ }
+ case STATE_DEAD: {
+ throw new IllegalStateException("Already dead");
+ }
+ default: {
+ throw new IllegalStateException("Unknown state: " + this.state);
+ }
+ }
+ }
+
+ private boolean isDead() {
+ return this.state == STATE_DEAD;
+ }
+
+ private boolean isTicking() {
+ return this.state == STATE_TICKING;
+ }
+
+ private void removeDeadSection(final ThreadedRegionSection section) {
+ this.deadSections.remove(section);
+ }
+
+ private void addDeadSection(final ThreadedRegionSection section) {
+ this.deadSections.add(section);
+ }
+
+ private void addSection(final ThreadedRegionSection section) {
+ if (section.getRegionPlain() != null) {
+ throw new IllegalStateException("Section already has region");
+ }
+ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) {
+ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey));
+ }
+ section.setRegionRelease(this);
+ }
+
+ public R getData() {
+ return this.data;
+ }
+
+ public boolean tryMarkTicking(final BooleanSupplier abort) {
+ this.regioniser.acquireWriteLock();
+ try {
+ if (this.state != STATE_READY || abort.getAsBoolean()) {
+ return false;
+ }
+
+ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) {
+ throw new IllegalStateException("Region " + this + " should not be ready");
+ }
+
+ this.state = STATE_TICKING;
+ return true;
+ } finally {
+ this.regioniser.releaseWriteLock();
+ }
+ }
+
+ public boolean markNotTicking() {
+ this.regioniser.acquireWriteLock();
+ try {
+ if (this.state != STATE_TICKING) {
+ throw new IllegalStateException("Attempting to release non-locked state");
+ }
+
+ this.regioniser.onRegionRelease(this);
+
+ return this.state == STATE_READY;
+ } catch (final Throwable throwable) {
+ LOGGER.error("Failed to acquire region " + this, throwable);
+ SneakyThrow.sneaky(throwable);
+ return false; // unreachable
+ } finally {
+ this.regioniser.releaseWriteLock();
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder ret = new StringBuilder(128);
+
+ ret.append("ThreadedRegion{");
+ ret.append("state=").append(this.state).append(',');
+ // To avoid recursion in toString, maybe fix later?
+ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(',');
+ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(',');
+
+ ret.append("sectionCount=").append(this.sectionByKey.size()).append(',');
+ ret.append("sections=[");
+ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) {
+ final ThreadedRegionSection section = iterator.next();
+
+ ret.append(section.toString());
+ if (iterator.hasNext()) {
+ ret.append(',');
+ }
+ }
+ ret.append(']');
+
+ ret.append('}');
+ return ret.toString();
+ }
+ }
+
+ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> {
+
+ public final int sectionX;
+ public final int sectionZ;
+ public final long sectionKey;
+ private final long[] chunksBitset;
+ private int chunkCount;
+ private int nonEmptyNeighbours;
+
+ private ThreadedRegion region;
+ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class);
+
+ public final ThreadedRegionizer regioniser;
+
+ private final int regionChunkShift;
+ private final int regionChunkMask;
+
+ private final S data;
+
+ private ThreadedRegion getRegionPlain() {
+ return (ThreadedRegion)REGION_HANDLE.get(this);
+ }
+
+ private ThreadedRegion getRegionAcquire() {
+ return (ThreadedRegion)REGION_HANDLE.getAcquire(this);
+ }
+
+ private void setRegionRelease(final ThreadedRegion value) {
+ REGION_HANDLE.setRelease(this, value);
+ }
+
+ // creates an empty section with zero non-empty neighbours
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) {
+ this.sectionX = sectionX;
+ this.sectionZ = sectionZ;
+ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)];
+ this.regioniser = regioniser;
+ this.regionChunkShift = regioniser.sectionChunkShift;
+ this.regionChunkMask = regioniser.regionSectionChunkSize - 1;
+ this.data = regioniser.callbacks
+ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift);
+ }
+
+ // creates a section with an initial chunk with zero non-empty neighbours
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser,
+ final int chunkXInit, final int chunkZInit) {
+ this(sectionX, sectionZ, regioniser);
+
+ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit);
+ this.chunkCount = 1;
+ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE
+ }
+
+ // creates an empty section with the specified number of non-empty neighbours
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser,
+ final int nonEmptyNeighbours) {
+ this(sectionX, sectionZ, regioniser);
+
+ this.nonEmptyNeighbours = nonEmptyNeighbours;
+ }
+
+ private boolean isEmpty() {
+ return this.chunkCount == 0;
+ }
+
+ private boolean hasOnlyOneChunk() {
+ return this.chunkCount == 1;
+ }
+
+ public boolean hasNonEmptyNeighbours() {
+ return this.nonEmptyNeighbours != 0;
+ }
+
+ /**
+ * Returns the section data associated with this region section. May be {@code null}.
+ */
+ public S getData() {
+ return this.data;
+ }
+
+ /**
+ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results.
+ */
+ public ThreadedRegion getRegion() {
+ return this.getRegionAcquire();
+ }
+
+ private int getChunkIndex(final int chunkX, final int chunkZ) {
+ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift);
+ }
+
+ private void markAlive() {
+ this.getRegionPlain().removeDeadSection(this);
+ }
+
+ private void markDead() {
+ this.getRegionPlain().addDeadSection(this);
+ }
+
+ private void incrementNonEmptyNeighbours() {
+ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) {
+ this.markAlive();
+ }
+ final int createRadius = this.regioniser.emptySectionCreateRadius;
+ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) {
+ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius);
+ }
+ }
+
+ private void decrementNonEmptyNeighbours() {
+ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) {
+ this.markDead();
+ }
+ if (this.nonEmptyNeighbours < 0) {
+ throw new IllegalStateException("Non empty neighbours reached zero");
+ }
+ }
+
+ /**
+ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create
+ * dead sections / increase non-empty neighbour count for neighbouring sections.
+ */
+ private boolean addChunk(final int chunkX, final int chunkZ) {
+ final int index = this.getChunkIndex(chunkX, chunkZ);
+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE
+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1)));
+ if (after == bitset) {
+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
+ }
+ final boolean notEmpty = ++this.chunkCount == 1;
+ if (notEmpty && this.nonEmptyNeighbours == 0) {
+ this.markAlive();
+ }
+ return notEmpty;
+ }
+
+ /**
+ * Returns whether the chunk count is now zero. Effectively returns whether
+ * the caller needs to decrement the neighbour count for neighbouring sections.
+ */
+ private boolean removeChunk(final int chunkX, final int chunkZ) {
+ final int index = this.getChunkIndex(chunkX, chunkZ);
+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE
+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1)));
+ if (before == bitset) {
+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
+ }
+ final boolean empty = --this.chunkCount == 0;
+ if (empty && this.nonEmptyNeighbours == 0) {
+ this.markDead();
+ }
+ return empty;
+ }
+
+ @Override
+ public String toString() {
+ return "RegionSection{" +
+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," +
+ "chunkCount=" + this.chunkCount + "," +
+ "chunksBitset=" + toString(this.chunksBitset) + "," +
+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," +
+ "hash=" + this.hashCode() +
+ "}";
+ }
+
+ public String toStringWithRegion() {
+ return "RegionSection{" +
+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," +
+ "chunkCount=" + this.chunkCount + "," +
+ "chunksBitset=" + toString(this.chunksBitset) + "," +
+ "hash=" + this.hashCode() + "," +
+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," +
+ "region=" + this.getRegionAcquire() +
+ "}";
+ }
+
+ private static String toString(final long[] array) {
+ final StringBuilder ret = new StringBuilder();
+ final char[] zeros = new char[Long.SIZE / 4];
+ for (final long value : array) {
+ // zero pad the hex string
+ Arrays.fill(zeros, '0');
+ final String string = Long.toHexString(value);
+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length());
+
+ ret.append(zeros);
+ }
+
+ return ret.toString();
+ }
+ }
+
+ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> {
+
+ /**
+ * Splits this region data into the specified regions set.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param regioniser Regioniser for which the regions reside in.
+ * @param into A map of region section coordinate key to the region that owns the section.
+ * @param regions The set of regions to split into.
+ */
+ public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into,
+ final ReferenceOpenHashSet> regions);
+
+ /**
+ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined
+ * except that its region data is already created.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param into Specified region.
+ */
+ public void mergeInto(final ThreadedRegion into);
+ }
+
+ public static interface ThreadedRegionSectionData {}
+
+ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> {
+
+ /**
+ * Creates new section data for the specified section x and section z.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param sectionX x coordinate of the section.
+ * @param sectionZ z coordinate of the section.
+ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that
+ * produces a section coordinate.
+ * @return New section data, may be {@code null}.
+ */
+ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift);
+
+ /**
+ * Creates new region data for the specified region.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param forRegion The region to create the data for.
+ * @return New region data, may be {@code null}.
+ */
+ public R createNewData(final ThreadedRegion forRegion);
+
+ /**
+ * Callback for when a region is created. This is invoked after the region is completely set up,
+ * so its data and owned sections are reliable to inspect.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param region The region that was created.
+ */
+ public void onRegionCreate(final ThreadedRegion region);
+
+ /**
+ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so
+ * its data and owned sections are reliable to inspect.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param region The region that is about to be destroyed.
+ */
+ public void onRegionDestroy(final ThreadedRegion region);
+
+ /**
+ * Callback for when a region is considered "active." An active region x is a non-destroyed region which
+ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are
+ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any
+ * other region considering the regioniser's empty section radius.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param region The region that is now active.
+ */
+ public void onRegionActive(final ThreadedRegion region);
+
+ /**
+ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but
+ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively,
+ * an inactive region may not tick and needs to be merged into its neighbouring regions.
+ *
+ * Note:
+ *
+ *
+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
+ * should NOT retrieve or modify ANY world state.
+ *
+ * @param region The region that is now inactive.
+ */
+ public void onRegionInactive(final ThreadedRegion region);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickData.java b/src/main/java/io/papermc/paper/threadedregions/TickData.java
new file mode 100644
index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TickData.java
@@ -0,0 +1,333 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import io.papermc.paper.util.IntervalledCounter;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class TickData {
+
+ private final long interval; // ns
+
+ private final ArrayDeque timeData = new ArrayDeque<>();
+
+ public TickData(final long intervalNS) {
+ this.interval = intervalNS;
+ }
+
+ public void addDataFrom(final TickRegionScheduler.TickTime time) {
+ final long start = time.tickStart();
+
+ TickRegionScheduler.TickTime first;
+ while ((first = this.timeData.peekFirst()) != null) {
+ // only remove data completely out of window
+ if ((start - first.tickEnd()) <= this.interval) {
+ break;
+ }
+ this.timeData.pollFirst();
+ }
+
+ this.timeData.add(time);
+ }
+
+ // fromIndex inclusive, toIndex exclusive
+ // will throw if arr.length == 0
+ private static double median(final long[] arr, final int fromIndex, final int toIndex) {
+ final int len = toIndex - fromIndex;
+ final int middle = fromIndex + (len >>> 1);
+ if ((len & 1) == 0) {
+ // even, average the two middle points
+ return (double)(arr[middle - 1] + arr[middle]) / 2.0;
+ } else {
+ // odd, just grab the middle
+ return (double)arr[middle];
+ }
+ }
+
+ // will throw if arr.length == 0
+ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex,
+ final boolean inverse) {
+ final int len = toIndex - fromIndex;
+ long sum = 0L;
+ final double median = median(arr, fromIndex, toIndex);
+ long min = arr[0];
+ long max = arr[0];
+
+ for (int i = fromIndex; i < toIndex; ++i) {
+ final long val = arr[i];
+ sum += val;
+ if (val < min) {
+ min = val;
+ }
+ if (val > max) {
+ max = val;
+ }
+ }
+
+ if (inverse) {
+ // for positive a,b we have that a >= b if and only if 1/a <= 1/b
+ return new SegmentData(
+ len,
+ (double)len / ((double)sum / 1.0E9),
+ 1.0E9 / median,
+ 1.0E9 / (double)max,
+ 1.0E9 / (double)min
+ );
+ } else {
+ return new SegmentData(
+ len,
+ (double)sum / (double)len,
+ median,
+ (double)min,
+ (double)max
+ );
+ }
+ }
+
+ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd,
+ final int percent99BestStart, final int percent99BestEnd,
+ final int percent95BestStart, final int percent95BestEnd,
+ final int percent1WorstStart, final int percent1WorstEnd,
+ final int percent5WorstStart, final int percent5WorstEnd,
+ final boolean inverse) {
+ return new SegmentedAverage(
+ computeSegmentData(data, allStart, allEnd, inverse),
+ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse),
+ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse),
+ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse),
+ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse)
+ );
+ }
+
+ private static record TickInformation(
+ long differenceFromLastTick,
+ long tickTime,
+ long tickTimeCPU
+ ) {}
+
+ // rets null if there is no data
+ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) {
+ if (this.timeData.isEmpty() && inProgress == null) {
+ return null;
+ }
+
+ final List allData = new ArrayList<>(this.timeData);
+ if (inProgress != null) {
+ allData.add(inProgress);
+ }
+
+ final long intervalStart = allData.get(0).tickStart();
+ final long intervalEnd = allData.get(allData.size() - 1).tickEnd();
+
+ // to make utilisation accurate, we need to take the total time used over the last interval period -
+ // this means if a tick start before the measurement interval, but ends within the interval, then we
+ // only consider the time it spent ticking inside the interval
+ long totalTimeOverInterval = 0L;
+ long measureStart = endTime - this.interval;
+
+ for (int i = 0, len = allData.size(); i < len; ++i) {
+ final TickRegionScheduler.TickTime time = allData.get(i);
+ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) {
+ final long diff = time.tickEnd() - measureStart;
+ if (diff > 0L) {
+ totalTimeOverInterval += diff;
+ } // else: the time is entirely out of interval
+ } else {
+ totalTimeOverInterval += time.tickLength();
+ }
+ }
+
+ // we only care about ticks, but because of inbetween tick task execution
+ // there will be data in allData that isn't ticks. But, that data cannot
+ // be ignored since it contributes to utilisation.
+ // So, we will "compact" the data by merging any inbetween tick times
+ // the next tick.
+ // If there is no "next tick", then we will create one.
+ final List collapsedData = new ArrayList<>();
+ for (int i = 0, len = allData.size(); i < len; ++i) {
+ final List toCollapse = new ArrayList<>();
+ TickRegionScheduler.TickTime lastTick = null;
+ for (;i < len; ++i) {
+ final TickRegionScheduler.TickTime time = allData.get(i);
+ if (!time.isTickExecution()) {
+ toCollapse.add(time);
+ continue;
+ }
+ lastTick = time;
+ break;
+ }
+
+ if (toCollapse.isEmpty()) {
+ // nothing to collapse
+ final TickRegionScheduler.TickTime last = allData.get(i);
+ collapsedData.add(
+ new TickInformation(
+ last.differenceFromLastTick(),
+ last.tickLength(),
+ last.supportCPUTime() ? last.tickCpuTime() : 0L
+ )
+ );
+ } else {
+ long totalTickTime = 0L;
+ long totalCpuTime = 0L;
+ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) {
+ final TickRegionScheduler.TickTime time = toCollapse.get(k);
+ totalTickTime += time.tickLength();
+ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L;
+ }
+ if (i < len) {
+ // we know there is a tick to collapse into
+ final TickRegionScheduler.TickTime last = allData.get(i);
+ collapsedData.add(
+ new TickInformation(
+ last.differenceFromLastTick(),
+ last.tickLength() + totalTickTime,
+ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime
+ )
+ );
+ } else {
+ // we do not have a tick to collapse into, so we must make one up
+ // we will assume that the tick is "starting now" and ongoing
+
+ // compute difference between imaginary tick and last tick
+ final long differenceBetweenTicks;
+ if (lastTick != null) {
+ // we have a last tick, use it
+ differenceBetweenTicks = lastTick.tickStart();
+ } else {
+ // we don't have a last tick, so we must make one up that makes sense
+ // if the current interval exceeds the max tick time, then use it
+
+ // Otherwise use the interval length.
+ // This is how differenceFromLastTick() works on TickTime when there is no previous interval.
+ differenceBetweenTicks = Math.max(
+ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime
+ );
+ }
+
+ collapsedData.add(
+ new TickInformation(
+ differenceBetweenTicks,
+ totalTickTime,
+ totalCpuTime
+ )
+ );
+ }
+ }
+ }
+
+
+ final int collectedTicks = collapsedData.size();
+ final long[] tickStartToStartDifferences = new long[collectedTicks];
+ final long[] timePerTickDataRaw = new long[collectedTicks];
+ final long[] missingCPUTimeDataRaw = new long[collectedTicks];
+
+ long totalTimeTicking = 0L;
+
+ int i = 0;
+ for (final TickInformation time : collapsedData) {
+ tickStartToStartDifferences[i] = time.differenceFromLastTick();
+ final long timePerTick = timePerTickDataRaw[i] = time.tickTime();
+ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU());
+
+ ++i;
+
+ totalTimeTicking += timePerTick;
+ }
+
+ Arrays.sort(tickStartToStartDifferences);
+ Arrays.sort(timePerTickDataRaw);
+ Arrays.sort(missingCPUTimeDataRaw);
+
+ // Note: computeSegmentData cannot take start == end
+ final int allStart = 0;
+ final int allEnd = collectedTicks;
+ final int percent95BestStart = 0;
+ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks);
+ final int percent99BestStart = 0;
+ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end
+ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks);
+ final int percent1WorstStart = (int)(0.99 * collectedTicks);
+ final int percent1WorstEnd = collectedTicks;
+ final int percent5WorstStart = (int)(0.95 * collectedTicks);
+ final int percent5WorstEnd = collectedTicks;
+
+ final SegmentedAverage tpsData = computeSegmentedAverage(
+ tickStartToStartDifferences,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ true
+ );
+
+ final SegmentedAverage timePerTickData = computeSegmentedAverage(
+ timePerTickDataRaw,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ false
+ );
+
+ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage(
+ missingCPUTimeDataRaw,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ false
+ );
+
+ final double utilisation = (double)totalTimeOverInterval / (double)this.interval;
+
+ return new TickReportData(
+ collectedTicks,
+ intervalStart,
+ intervalEnd,
+ totalTimeTicking,
+ utilisation,
+
+ tpsData,
+ timePerTickData,
+ missingCPUTimeData
+ );
+ }
+
+ public static final record TickReportData(
+ int collectedTicks,
+ long collectedTickIntervalStart,
+ long collectedTickIntervalEnd,
+ long totalTimeTicking,
+ double utilisation,
+
+ SegmentedAverage tpsData,
+ // in ns
+ SegmentedAverage timePerTickData,
+ // in ns
+ SegmentedAverage missingCPUTimeData
+ ) {}
+
+ public static final record SegmentedAverage(
+ SegmentData segmentAll,
+ SegmentData segment99PercentBest,
+ SegmentData segment95PercentBest,
+ SegmentData segment5PercentWorst,
+ SegmentData segment1PercentWorst
+ ) {}
+
+ public static final record SegmentData(
+ int count,
+ double average,
+ double median,
+ double least,
+ double greatest
+ ) {}
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f14eda409ca49095dc43796868435b705c36ce4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java
@@ -0,0 +1,559 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadMXBean;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BooleanSupplier;
+
+public final class TickRegionScheduler {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean();
+ private static final boolean MEASURE_CPU_TIME;
+ static {
+ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported();
+ if (MEASURE_CPU_TIME) {
+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
+ } else {
+ LOGGER.warn("TickRegionScheduler CPU time measurement is not available");
+ }
+ }
+
+ public static final int TICK_RATE = 20;
+ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns
+
+ private final SchedulerThreadPool scheduler;
+
+ public TickRegionScheduler(final int threads) {
+ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() {
+ private final AtomicInteger idGenerator = new AtomicInteger();
+
+ @Override
+ public Thread newThread(final Runnable run) {
+ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement());
+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
+ return ret;
+ }
+ });
+ }
+
+ public int getTotalThreadCount() {
+ return this.scheduler.getThreads().length;
+ }
+
+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) {
+ final Thread currThread = Thread.currentThread();
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
+ throw new IllegalStateException("Must be tick thread runner");
+ }
+ if (region != null && tickThreadRunner.currentTickingRegion != null) {
+ throw new IllegalStateException("Trying to double set ticking region!");
+ }
+ if (region == null && tickThreadRunner.currentTickingRegion == null) {
+ throw new IllegalStateException("Trying to double unset ticking region!");
+ }
+ tickThreadRunner.currentTickingRegion = region;
+ if (region != null) {
+ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get();
+ } else {
+ tickThreadRunner.currentTickingWorldRegionizedData = null;
+ }
+ }
+
+ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) {
+ final Thread currThread = Thread.currentThread();
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
+ throw new IllegalStateException("Must be tick thread runner");
+ }
+ if (task != null && tickThreadRunner.currentTickingTask != null) {
+ throw new IllegalStateException("Trying to double set ticking task!");
+ }
+ if (task == null && tickThreadRunner.currentTickingTask == null) {
+ throw new IllegalStateException("Trying to double unset ticking task!");
+ }
+ tickThreadRunner.currentTickingTask = task;
+ }
+
+ /**
+ * Returns the current ticking region, or {@code null} if there is no ticking region.
+ * If this thread is not a TickThread, then returns {@code null}.
+ */
+ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() {
+ final Thread currThread = Thread.currentThread();
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
+ return RegionShutdownThread.getRegion();
+ }
+ return tickThreadRunner.currentTickingRegion;
+ }
+
+ /**
+ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region.
+ * This is a faster alternative to calling the {@link RegionizedData#get()} method.
+ * If this thread is not a TickThread, then returns {@code null}.
+ */
+ public static RegionizedWorldData getCurrentRegionizedWorldData() {
+ final Thread currThread = Thread.currentThread();
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
+ return RegionShutdownThread.getWorldData();
+ }
+ return tickThreadRunner.currentTickingWorldRegionizedData;
+ }
+
+ /**
+ * Returns the current ticking task, or {@code null} if there is no ticking region.
+ * If this thread is not a TickThread, then returns {@code null}.
+ */
+ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() {
+ final Thread currThread = Thread.currentThread();
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
+ return null;
+ }
+ return tickThreadRunner.currentTickingTask;
+ }
+
+ /**
+ * Schedules the given region
+ * @throws IllegalStateException If the region is already scheduled or is ticking
+ */
+ public void scheduleRegion(final RegionScheduleHandle region) {
+ region.scheduler = this;
+ this.scheduler.schedule(region);
+ }
+
+ /**
+ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task
+ * execution, then it will be cancelled after.
+ */
+ public void descheduleRegion(final RegionScheduleHandle region) {
+ // To avoid acquiring any of the locks the scheduler may be using, we
+ // simply cancel the next action.
+ region.markNonSchedulable();
+ }
+
+ /**
+ * Updates the tick start to the farthest into the future of its current scheduled time and the
+ * provided time.
+ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its
+ * current start time, {@code true} if the next tick start was adjusted.
+ */
+ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) {
+ return this.scheduler.updateTickStartToMax(region, newStart);
+ }
+
+ public boolean halt(final boolean sync, final long maxWaitNS) {
+ return this.scheduler.halt(sync, maxWaitNS);
+ }
+
+ public void setHasTasks(final RegionScheduleHandle region) {
+ this.scheduler.notifyTasks(region);
+ }
+
+ public void init() {
+ this.scheduler.start();
+ }
+
+ private void uncaughtException(final Thread thread, final Throwable thr) {
+ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr);
+
+ // prevent further ticks from occurring
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
+ this.scheduler.halt(false, 0L);
+
+ MinecraftServer.getServer().stopServer();
+ }
+
+ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) {
+ // when a region fails, we need to shut down the server gracefully
+
+ // prevent further ticks from occurring
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
+ this.scheduler.halt(false, 0L);
+
+ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk();
+
+ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr);
+
+ MinecraftServer.getServer().stopServer();
+ }
+
+ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal.
+ // This is much faster than a thread local, since the thread local has to use a map lookup.
+ private static final class TickThreadRunner extends TickThread {
+
+ private ThreadedRegionizer.ThreadedRegion currentTickingRegion;
+ private RegionizedWorldData currentTickingWorldRegionizedData;
+ private SchedulerThreadPool.SchedulableTick currentTickingTask;
+
+ public TickThreadRunner(final Runnable run, final String name) {
+ super(run, name);
+ }
+ }
+
+ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick {
+
+ protected long currentTick;
+ protected long lastTickStart;
+
+ protected final TickData tickTimes5s;
+ protected final TickData tickTimes15s;
+ protected final TickData tickTimes1m;
+ protected final TickData tickTimes5m;
+ protected final TickData tickTimes15m;
+ protected TickTime currentTickData;
+ protected Thread currentTickingThread;
+
+ public final TickRegions.TickRegionData region;
+ private final AtomicBoolean cancelled = new AtomicBoolean();
+
+ protected final Schedule tickSchedule;
+
+ private TickRegionScheduler scheduler;
+
+ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) {
+ this.currentTick = 0L;
+ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET;
+ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L));
+ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L));
+ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L));
+ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L));
+ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L));
+ this.region = region;
+
+ this.setScheduledStart(firstStart);
+ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS);
+ }
+
+ /**
+ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)}
+ * so that the tick schedule and scheduled start remain synchronised
+ */
+ protected final void updateScheduledStart(final long to) {
+ this.setScheduledStart(to);
+ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS);
+ }
+
+ public final void markNonSchedulable() {
+ this.cancelled.set(true);
+ }
+
+ public final boolean isMarkedAsNonSchedulable() {
+ return this.cancelled.get();
+ }
+
+ protected abstract boolean tryMarkTicking();
+
+ protected abstract boolean markNotTicking();
+
+ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd);
+
+ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue);
+
+ protected abstract boolean hasIntermediateTasks();
+
+ @Override
+ public final boolean hasTasks() {
+ return this.hasIntermediateTasks();
+ }
+
+ @Override
+ public final Boolean runTasks(final BooleanSupplier canContinue) {
+ if (this.cancelled.get()) {
+ return null;
+ }
+
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+ final long tickStart = System.nanoTime();
+
+ if (!this.tryMarkTicking()) {
+ if (!this.cancelled.get()) {
+ throw new IllegalStateException("Scheduled region should be acquirable");
+ }
+ // region was killed
+ return null;
+ }
+
+ TickRegionScheduler.setTickTask(this);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(this.region.region);
+ }
+
+ synchronized (this) {
+ this.currentTickData = new TickTime(
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart,
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
+ false
+ );
+ this.currentTickingThread = Thread.currentThread();
+ }
+
+ final boolean ret;
+ try {
+ ret = this.runRegionTasks(() -> {
+ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
+ });
+ } catch (final Throwable thr) {
+ this.scheduler.regionFailed(this, true, thr);
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ // don't release region for another tick
+ return null;
+ } finally {
+ TickRegionScheduler.setTickTask(null);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(null);
+ }
+ final long tickEnd = System.nanoTime();
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+
+ final TickTime time = new TickTime(
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET,
+ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false
+ );
+
+ this.addTickTime(time);
+ }
+
+ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret);
+ }
+
+ @Override
+ public final boolean runTick() {
+ // Remember, we are supposed use setScheduledStart if we return true here, otherwise
+ // the scheduler will try to schedule for the same time.
+ if (this.cancelled.get()) {
+ return false;
+ }
+
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+ final long tickStart = System.nanoTime();
+
+ // use max(), don't assume that tickStart >= scheduledStart
+ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart));
+
+ if (!this.tryMarkTicking()) {
+ if (!this.cancelled.get()) {
+ throw new IllegalStateException("Scheduled region should be acquirable");
+ }
+ // region was killed
+ return false;
+ }
+ if (this.cancelled.get()) {
+ this.markNotTicking();
+ // region should be killed
+ return false;
+ }
+
+ TickRegionScheduler.setTickTask(this);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(this.region.region);
+ }
+ this.incrementTickCount();
+ final long lastTickStart = this.lastTickStart;
+ this.lastTickStart = tickStart;
+
+ final long scheduledStart = this.getScheduledStart();
+ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS;
+
+ synchronized (this) {
+ this.currentTickData = new TickTime(
+ lastTickStart, scheduledStart, tickStart, cpuStart,
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
+ true
+ );
+ this.currentTickingThread = Thread.currentThread();
+ }
+
+ try {
+ // next start isn't updated until the end of this tick
+ this.tickRegion(tickCount, tickStart, scheduledEnd);
+ } catch (final Throwable thr) {
+ this.scheduler.regionFailed(this, false, thr);
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further
+ return false;
+ } finally {
+ TickRegionScheduler.setTickTask(null);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(null);
+ }
+ final long tickEnd = System.nanoTime();
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+
+ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions
+ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest
+ // of the current time and "ideal" next tick start.
+ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS);
+ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS)));
+
+ final TickTime time = new TickTime(
+ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true
+ );
+
+ this.addTickTime(time);
+ }
+
+ // Only AFTER updating the tickStart
+ return this.markNotTicking() && !this.cancelled.get();
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ private void addTickTime(final TickTime time) {
+ synchronized (this) {
+ this.currentTickData = null;
+ this.currentTickingThread = null;
+ this.tickTimes5s.addDataFrom(time);
+ this.tickTimes15s.addDataFrom(time);
+ this.tickTimes1m.addDataFrom(time);
+ this.tickTimes5m.addDataFrom(time);
+ this.tickTimes15m.addDataFrom(time);
+ }
+ }
+
+ private TickTime adjustCurrentTickData(final long tickEnd) {
+ final TickTime currentTickData = this.currentTickData;
+ if (currentTickData == null) {
+ return null;
+ }
+
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L;
+
+ return new TickTime(
+ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(),
+ currentTickData.tickStart(), currentTickData.tickStartCPU(),
+ tickEnd, cpuEnd,
+ MEASURE_CPU_TIME, currentTickData.isTickExecution()
+ );
+ }
+
+ public final TickData.TickReportData getTickReport5s(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport15s(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport1m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport5m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport15m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ private void incrementTickCount() {
+ ++this.currentTick;
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ public final long getCurrentTick() {
+ return this.currentTick;
+ }
+
+ protected final void setCurrentTick(final long value) {
+ this.currentTick = value;
+ }
+ }
+
+ // All time units are in nanoseconds.
+ public static final record TickTime(
+ long previousTickStart,
+ long scheduledTickStart,
+ long tickStart,
+ long tickStartCPU,
+ long tickEnd,
+ long tickEndCPU,
+ boolean supportCPUTime,
+ boolean isTickExecution
+ ) {
+ /**
+ * The difference between the start tick time and the scheduled start tick time. This value is
+ * < 0 if the tick started before the scheduled tick time.
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public final long startOvershoot() {
+ return this.tickStart - this.scheduledTickStart;
+ }
+
+ /**
+ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong).
+ */
+ public final long tickLength() {
+ return this.tickEnd - this.tickStart;
+ }
+
+ /**
+ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength,
+ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE
+ * if CPU time measurement is not supported.
+ */
+ public final long tickCpuTime() {
+ if (!this.supportCPUTime()) {
+ return Long.MIN_VALUE;
+ }
+ return this.tickEndCPU - this.tickStartCPU;
+ }
+
+ /**
+ * The difference in time from the start of the last tick to the start of the current tick. If there is no
+ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength).
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public final long differenceFromLastTick() {
+ if (this.hasLastTick()) {
+ return this.tickStart - this.previousTickStart;
+ }
+ return Math.max(TIME_BETWEEN_TICKS, this.tickLength());
+ }
+
+ /**
+ * Returns whether there was a tick that occurred before this one.
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public boolean hasLastTick() {
+ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET;
+ }
+
+ /*
+ * Remember, this is the expected behavior of the following:
+ *
+ * MSPT: Time per tick. This does not include overshoot time, just the tickLength().
+ *
+ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick).
+ */
+ }
+}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ac3c3612df4c1c5c28efc4e23ea107aa722050e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
@@ -0,0 +1,355 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
+import io.papermc.paper.configuration.GlobalConfiguration;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import org.slf4j.Logger;
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BooleanSupplier;
+
+public final class TickRegions implements ThreadedRegionizer.RegionCallbacks {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public static int getRegionChunkShift() {
+ return 4;
+ }
+
+ private static boolean initialised;
+ private static TickRegionScheduler scheduler;
+
+ public static TickRegionScheduler getScheduler() {
+ return scheduler;
+ }
+
+ public static void init(final GlobalConfiguration.ThreadedRegions config) {
+ if (initialised) {
+ return;
+ }
+ initialised = true;
+
+ int tickThreads;
+ if (config.threads <= 0) {
+ tickThreads = Runtime.getRuntime().availableProcessors() / 2;
+ if (tickThreads <= 4) {
+ tickThreads = 1;
+ } else {
+ tickThreads = (2 * tickThreads) / 3;
+ }
+ } else {
+ tickThreads = config.threads;
+ }
+
+ scheduler = new TickRegionScheduler(tickThreads);
+ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads");
+ }
+
+ @Override
+ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) {
+ return new TickRegionData(region);
+ }
+
+ @Override
+ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) {
+ return null;
+ }
+
+ @Override
+ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) {
+ // nothing for now
+ }
+
+ @Override
+ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) {
+ // nothing for now
+ }
+
+ @Override
+ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) {
+ final TickRegionData data = region.getData();
+
+ data.tickHandle.checkInitialSchedule();
+ scheduler.scheduleRegion(data.tickHandle);
+ }
+
+ @Override
+ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) {
+ final TickRegionData data = region.getData();
+
+ scheduler.descheduleRegion(data.tickHandle);
+ // old handle cannot be scheduled anymore, copy to a new handle
+ data.tickHandle = data.tickHandle.copy();
+ }
+
+ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {}
+
+ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData {
+
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
+ /** Never 0L, since 0L is reserved for global region. */
+ public final long id = ID_GENERATOR.incrementAndGet();
+
+ public final ThreadedRegionizer.ThreadedRegion region;
+ public final ServerLevel world;
+
+ // generic regionised data
+ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>();
+
+ // tick data
+ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET);
+
+ // queue data
+ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData;
+
+ // chunk holder manager data
+ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData();
+
+ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) {
+ this.region = region;
+ this.world = region.regioniser.world;
+ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData);
+ }
+
+ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() {
+ return this.taskQueueData;
+ }
+
+ // the value returned can be invalidated at any time, except when the caller
+ // is ticking this region
+ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() {
+ return this.tickHandle;
+ }
+
+ public long getCurrentTick() {
+ return this.tickHandle.getCurrentTick();
+ }
+
+ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() {
+ return this.holderManagerRegionData;
+ }
+
+ T getOrCreateRegionizedData(final RegionizedData regionizedData) {
+ T ret = (T)this.regionizedData.get(regionizedData);
+
+ if (ret != null) {
+ return ret;
+ }
+
+ ret = regionizedData.createNewValue();
+ this.regionizedData.put(regionizedData, ret);
+
+ return ret;
+ }
+
+ @Override
+ public void split(final ThreadedRegionizer regioniser,
+ final Long2ReferenceOpenHashMap> into,
+ final ReferenceOpenHashSet> regions) {
+ final int shift = regioniser.sectionChunkShift;
+
+ // tick data
+ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled),
+ // and the other regions to split into are not scheduled yet.
+ for (final ThreadedRegionizer.ThreadedRegion region : regions) {
+ final TickRegionData data = region.getData();
+ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle);
+ }
+
+ // generic regionised data
+ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
+ dataIterator.hasNext();) {
+ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next();
+ final RegionizedData> data = regionDataEntry.getKey();
+ final Object from = regionDataEntry.getValue();
+
+ final ReferenceOpenHashSet