From be2a4b8a6d0f1d6e2e4ceea0bf35a8c3209af118 Mon Sep 17 00:00:00 2001 From: Techcable Date: Mon, 25 Apr 2016 23:46:00 -0700 Subject: [PATCH] Reduce the overhead of lots and lots of teams with the same names Featherboard (and other bad plugins) use persistent scoreboards (scoreboard.dat), causing every team ever to be sent to waterfall. This is bad, and takes tons of memory. Uses String.intern() to avoid duplicating strings Uses a sorted array to avoid the overhead of the hashset in a team. diff --git a/api/src/main/java/io/github/waterfallmc/waterfall/utils/LowMemorySet.java b/api/src/main/java/io/github/waterfallmc/waterfall/utils/LowMemorySet.java new file mode 100644 index 0000000..3a062b6 --- /dev/null +++ b/api/src/main/java/io/github/waterfallmc/waterfall/utils/LowMemorySet.java @@ -0,0 +1,177 @@ +package io.github.waterfallmc.waterfall.utils; + +import lombok.*; + +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A set that uses a binary search to find objects in a sorted array. + * Avoids the memory cost of {@link java.util.HashSet}, while maintaining reasonable {@link Set#contains} + * Insertions ma O(N)! + */ +public class LowMemorySet> extends AbstractSet implements Set { + private final List backing; + @Setter + private boolean trimAggressively; + + protected LowMemorySet(List list) { + this.backing = checkNotNull(list, "Null list"); + this.sort(); // We have to sort any initial elements + this.trim(true); + } + + public static > LowMemorySet create() { + return new LowMemorySet<>(new ArrayList()); + } + + public static > LowMemorySet copyOf(Collection c) { + return new LowMemorySet<>(new ArrayList<>(c)); + } + + @SuppressWarnings("unchecked") // nope + private int indexOf(Object o) { + return Collections.binarySearch((List) backing, o); + } + + private void sort() { + backing.sort(null); + this.trim(); + } + + private void trim() { + trim(false); + } + + private void trim(boolean force) { + if (backing instanceof ArrayList && force || trimAggressively) ((ArrayList) backing).trimToSize(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + @Override + public Iterator iterator() { + Iterator backing = this.backing.iterator(); + return new Iterator() { + private T last; + + @Override + public boolean hasNext() { + return backing.hasNext(); + } + + @Override + public T next() { + return (last = backing.next()); + } + + @Override + public void remove() { + LowMemorySet.this.remove(last); + } + + @Override + public void forEachRemaining(Consumer action) { + backing.forEachRemaining(action); + } + }; + } + + @Override + public Object[] toArray() { + return backing.toArray(); + } + + @Override + public T1[] toArray(T1[] a) { + return backing.toArray(a); + } + + @Override + public boolean add(T t) { + if (contains(t)) return false; + backing.add(t); + this.sort(); + return true; + } + + @Override + public boolean remove(Object o) { + T old = backing.remove(indexOf(o)); + this.trim(); + assert old == o; + return old != null; + } + + @Override + public boolean removeAll(Collection c) { + int oldSize = this.size(); + boolean result = backing.removeIf(c::contains); + this.trim(oldSize - this.size() > 10); + return result; + } + + @Override + public boolean retainAll(Collection c) { + int oldSize = this.size(); + boolean result = backing.removeIf((o) -> !c.contains(o)); + this.trim(oldSize - this.size() > 10); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(Collection c) { + if (containsAll(c)) return false; + backing.addAll(c); + this.sort(); + return true; + } + + @Override + public void clear() { + backing.clear(); + this.trim(true); + } + + @Override + public void forEach(Consumer action) { + backing.forEach(action); + } + + @Override + public Stream stream() { + return backing.stream(); + } + + @Override + public Stream parallelStream() { + return backing.parallelStream(); + } + + @Override + public boolean removeIf(Predicate filter) { + int oldSize = this.size(); + boolean worked = backing.removeIf(filter); + this.trim(this.size() - oldSize > 10); + return worked; + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/score/Team.java b/api/src/main/java/net/md_5/bungee/api/score/Team.java index 4166037..f0f019b 100644 --- a/api/src/main/java/net/md_5/bungee/api/score/Team.java +++ b/api/src/main/java/net/md_5/bungee/api/score/Team.java @@ -1,11 +1,12 @@ package net.md_5.bungee.api.score; +import lombok.*; + import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Set; -import lombok.Data; -import lombok.NonNull; + +import io.github.waterfallmc.waterfall.utils.LowMemorySet; @Data public class Team @@ -20,7 +21,7 @@ public class Team private String nameTagVisibility; private String collisionRule; private byte color; - private Set players = new HashSet<>(); + private Set players = LowMemorySet.create(); public Collection getPlayers() { @@ -29,7 +30,7 @@ public class Team public void addPlayer(String name) { - players.add( name ); + players.add(name.intern()); } public void removePlayer(String name) diff --git a/api/src/test/java/io/github/waterfallmc/waterfall/utils/LowMemorySetTest.java b/api/src/test/java/io/github/waterfallmc/waterfall/utils/LowMemorySetTest.java new file mode 100644 index 0000000..5aa306a --- /dev/null +++ b/api/src/test/java/io/github/waterfallmc/waterfall/utils/LowMemorySetTest.java @@ -0,0 +1,56 @@ +package io.github.waterfallmc.waterfall.utils; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class LowMemorySetTest { + + private static final ImmutableList ELEMENTS = ImmutableList.of("test", "bob", "road", "food", "sleep", "sore-thought", "pain"); + + @Test + public void testContains() { + LowMemorySet set = LowMemorySet.copyOf(ELEMENTS); + assertTrue(set.contains("test")); + assertTrue(set.contains("bob")); + assertFalse(set.contains("stupid")); + assertFalse(set.contains("head")); + } + + @Test + public void testRemove() { + LowMemorySet set = LowMemorySet.copyOf(ELEMENTS); + assertTrue(set.contains("test")); + set.remove("test"); + assertFalse(set.contains("test")); + assertTrue(set.contains("bob")); + set.remove("bob"); + assertFalse(set.contains("bob")); + assertTrue(ELEMENTS.size() - set.size() == 2); + assertTrue(set.contains("road")); + assertTrue(set.contains("food")); + assertTrue(set.contains("pain")); + set.removeAll(ImmutableList.of("road", "food", "pain")); + assertFalse(set.contains("road")); + assertFalse(set.contains("food")); + assertFalse(set.contains("pain")); + assertTrue(ELEMENTS.size() - set.size() == 5); + } + + @Test + public void testAdd() { + LowMemorySet set = LowMemorySet.copyOf(ELEMENTS); + assertFalse(set.contains("Techcable")); + set.add("Techcable"); + assertTrue(set.contains("Techcable")); + set.addAll(ImmutableList.of("Techcable", "PhanaticD", "Dragonslayer293", "Aikar")); + assertTrue(set.contains("Techcable")); + assertTrue(set.contains("PhanaticD")); + assertTrue(set.contains("Aikar")); + assertFalse(set.contains("md_5")); + } + +} -- 2.8.3