Improve memory usage of tags

Signed-off-by: TheMode <themode@outlook.fr>
This commit is contained in:
TheMode 2022-04-14 16:26:57 +02:00
parent 821063addf
commit 98a6e73e99
4 changed files with 126 additions and 51 deletions

View File

@ -1,6 +1,7 @@
package net.minestom.server.tag;
import net.minestom.server.utils.ArrayUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
@ -9,20 +10,20 @@ import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTCompoundLike;
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
import java.util.Arrays;
import java.util.function.UnaryOperator;
final class TagHandlerImpl implements TagHandler {
private volatile Entry<?>[] entries;
private volatile SPMCMap entries;
private Cache cache;
TagHandlerImpl(Entry<?>[] entries, Cache cache) {
TagHandlerImpl(SPMCMap entries, Cache cache) {
this.entries = entries;
this.cache = cache;
}
TagHandlerImpl() {
this(new Entry[0], null);
this.entries = new SPMCMap();
this.cache = null;
}
static TagHandlerImpl fromCompound(NBTCompoundLike compound) {
@ -88,11 +89,9 @@ final class TagHandlerImpl implements TagHandler {
return updatedCache().compound;
}
public synchronized <T> void write(@NotNull Tag<T> tag, @Nullable T value) {
private synchronized <T> void write(@NotNull Tag<T> tag, @Nullable T value) {
int tagIndex = tag.index;
TagHandlerImpl local = this;
Entry<?>[] entries = this.entries;
final Entry<?>[] localEntries = entries;
final Tag.PathEntry[] paths = tag.path;
TagHandlerImpl[] pathHandlers = null;
@ -102,11 +101,7 @@ final class TagHandlerImpl implements TagHandler {
for (int i = 0; i < length; i++) {
final Tag.PathEntry path = paths[i];
final int pathIndex = path.index();
if (pathIndex >= entries.length) {
if (value == null) return;
local.entries = entries = Arrays.copyOf(entries, pathIndex + 1);
}
final Entry<?> entry = entries[pathIndex];
final Entry<?> entry = local.entries.get(pathIndex);
if (entry instanceof PathEntry pathEntry) {
// Existing path, continue navigating
local = pathEntry.value;
@ -114,46 +109,36 @@ final class TagHandlerImpl implements TagHandler {
if (value == null) return;
// Empty path, create a new handler.
// Slow path is taken if the entry comes from a Structure tag, requiring conversion from NBT
local = entry != null && entry.updatedNbt() instanceof NBTCompound compound ? fromCompound(compound) : new TagHandlerImpl();
entries[pathIndex] = new PathEntry(path.name(), local);
TagHandlerImpl tmp = local;
local = entry != null && entry.updatedNbt() instanceof NBTCompound compound ?
fromCompound(compound) : new TagHandlerImpl();
tmp.entries.put(pathIndex, new PathEntry(path.name(), local));
}
entries = local.entries;
pathHandlers[i] = local;
}
// Handle removal if the tag was present (recursively)
if (value == null) {
// Remove entry
{
Entry<?>[] finalEntries = pathHandlers[length - 1].entries;
if (finalEntries.length > tagIndex) finalEntries[tagIndex] = null;
else return;
}
if (pathHandlers[length - 1].entries.remove(tagIndex) == null) return;
// Clear empty parents
boolean empty = false;
for (int i = length - 1; i >= 0; i--) {
TagHandlerImpl handler = pathHandlers[i];
Entry<?>[] entr = handler.entries;
// Verify if the handler is empty
empty = tagIndex >= entr.length || ArrayUtils.isEmpty(entr);
if (empty && i > 0) {
final TagHandlerImpl handler = pathHandlers[i];
if (!handler.entries.isEmpty()) break;
final int pathIndex = paths[i].index();
if (i == 0) {
// Remove the root handler
local = this;
tagIndex = pathIndex;
} else {
TagHandlerImpl parent = pathHandlers[i - 1];
parent.entries[paths[i].index()] = null;
parent.entries.remove(pathIndex);
}
}
if (empty) {
// Remove the root handler
local = this;
entries = localEntries;
tagIndex = paths[0].index();
}
}
}
// Normal tag
if (tagIndex >= entries.length) {
if (value == null) return;
local.entries = entries = Arrays.copyOf(entries, tagIndex + 1);
}
entries[tagIndex] = value != null ? new TagEntry<>(tag, value) : null;
if (value != null) local.entries.put(tagIndex, new TagEntry<>(tag, value));
else local.entries.remove(tagIndex);
this.cache = null;
if (pathHandlers != null) {
for (var handler : pathHandlers) handler.cache = null;
@ -163,29 +148,28 @@ final class TagHandlerImpl implements TagHandler {
private synchronized Cache updatedCache() {
Cache cache = this.cache;
if (cache == null) {
final Entry<?>[] entries = this.entries;
if (entries.length > 0) {
final var entries = this.entries;
if (!entries.isEmpty()) {
MutableNBTCompound tmp = new MutableNBTCompound();
for (Entry<?> entry : entries) {
for (Entry<?> entry : entries.values()) {
if (entry != null) tmp.put(entry.tag().getKey(), entry.updatedNbt());
}
cache = !tmp.isEmpty() ? new Cache(entries.clone(), tmp.toCompound()) : Cache.EMPTY;
cache = new Cache(entries.clone(), tmp.toCompound());
} else cache = Cache.EMPTY;
this.cache = cache;
}
return cache;
}
private static <T> T read(Entry<?>[] entries, Tag<T> tag) {
private static <T> T read(Int2ObjectOpenHashMap<Entry<?>> entries, Tag<T> tag) {
final Tag.PathEntry[] paths = tag.path;
if (paths != null) {
// Must be a path-able entry
if ((entries = traversePath(paths, entries)) == null)
return tag.createDefault();
}
final int index = tag.index;
final Entry<?> entry;
if (index >= entries.length || (entry = entries[index]) == null) {
if ((entry = entries.get(tag.index)) == null) {
return tag.createDefault();
}
if (entry.tag().shareValue(tag)) {
@ -203,11 +187,11 @@ final class TagHandlerImpl implements TagHandler {
}
}
private static Entry<?>[] traversePath(Tag.PathEntry[] paths, Entry<?>[] entries) {
private static Int2ObjectOpenHashMap<Entry<?>> traversePath(Tag.PathEntry[] paths,
Int2ObjectOpenHashMap<Entry<?>> entries) {
for (var path : paths) {
final int pathIndex = path.index();
final Entry<?> entry;
if (pathIndex >= entries.length || (entry = entries[pathIndex]) == null)
if ((entry = entries.get(path.index())) == null)
return null;
if (entry instanceof PathEntry pathEntry) {
entries = pathEntry.value.entries;
@ -224,8 +208,8 @@ final class TagHandlerImpl implements TagHandler {
return entries;
}
private record Cache(Entry<?>[] entries, NBTCompound compound) implements TagReadable {
static final Cache EMPTY = new Cache(new Entry[0], NBTCompound.EMPTY);
private record Cache(Int2ObjectOpenHashMap<Entry<?>> entries, NBTCompound compound) implements TagReadable {
static final Cache EMPTY = new Cache(new Int2ObjectOpenHashMap<>(), NBTCompound.EMPTY);
@Override
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
@ -282,4 +266,43 @@ final class TagHandlerImpl implements TagHandler {
return value.asCompound();
}
}
final class SPMCMap extends Int2ObjectOpenHashMap<Entry<?>> {
boolean rehashed;
SPMCMap() {
}
SPMCMap(Int2ObjectMap<TagHandlerImpl.Entry<?>> m) {
super(m);
}
@Override
public TagHandlerImpl.Entry<?> put(int k, TagHandlerImpl.Entry<?> entry) {
assertState();
return super.put(k, entry);
}
@Override
public boolean remove(int k, Object v) {
assertState();
return super.remove(k, v);
}
@Override
protected void rehash(int newSize) {
assertState();
TagHandlerImpl.this.entries = new SPMCMap(this);
this.rehashed = true;
}
@Override
public SPMCMap clone() {
return (SPMCMap) super.clone();
}
private void assertState() {
assert !rehashed;
}
}
}

View File

@ -3,6 +3,7 @@ package net.minestom.server.tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class TagHandlerCopyTest {
@ -14,4 +15,16 @@ public class TagHandlerCopyTest {
var handler2 = handler1.copy();
assertEquals(handler1.getTag(Tag.String("key")), handler2.getTag(Tag.String("key")));
}
@Test
public void copyRehashing() {
var handler = TagHandler.newHandler();
TagHandler handlerCopy;
for (int i = 0; i < 1000; i++) {
handlerCopy = handler.copy();
var tag = Tag.Integer("copyRehashing" + i);
handler.setTag(tag, i);
assertNull(handlerCopy.getTag(tag));
}
}
}

View File

@ -89,6 +89,34 @@ public class TagPathTest {
assertEqualsSNBT("{}", handler.asCompound());
}
@Test
public void secondPathClearSnbt() {
var handler = TagHandler.newHandler();
var numberTag = Tag.Integer("number").path("path1", "path2");
var stringTag = Tag.String("string").path("path1");
handler.setTag(numberTag, 5);
handler.setTag(stringTag, "test");
assertEqualsSNBT("""
{
"path1": {
"path2": {
"number":5
},
"string":"test"
}
}
""", handler.asCompound());
handler.removeTag(numberTag);
assertEqualsSNBT("""
{
"path1": {
"string":"test"
}
}
""", handler.asCompound());
}
@Test
public void differentPath() {
var handler = TagHandler.newHandler();

View File

@ -121,4 +121,15 @@ public class TagTest {
assertEquals(5, handler.getTag(Tag.Integer("tag1")));
assertEquals(1, handler.getTag(Tag.Integer("tag2")));
}
@Test
public void rehashing() {
var handler = TagHandler.newHandler();
for (int i = 0; i < 1000; i++) {
handler.setTag(Tag.Integer("rehashing" + i), i);
for (int j = i; j > 0; j--) {
assertEquals(j, handler.getTag(Tag.Integer("rehashing" + j)));
}
}
}
}