From eeaf6ec7c4ce8b15447399baffe5b63675ea7358 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 00000000..6c006923 --- /dev/null +++ b/api/src/main/java/io/github/waterfallmc/waterfall/utils/LowMemorySet.java @@ -0,0 +1,151 @@ +package io.github.waterfallmc.waterfall.utils; + +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 are O(N)! + */ +public class LowMemorySet> extends AbstractSet implements Set { + private final List backing; + + private LowMemorySet(List list) { + this.backing = checkNotNull(list, "Null list"); + this.sort(); // We have to sort any initial elements + } + + public static > LowMemorySet create() { + return new LowMemorySet<>(new ArrayList()); + } + + public static > LowMemorySet copyOf(Collection c) { + return new LowMemorySet<>(new ArrayList<>(c)); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private int indexOf(Object o) { + return Collections.binarySearch((List) backing, o); + } + + private void sort() { + backing.sort(null); + } + + @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() { + @Override + public boolean hasNext() { + return backing.hasNext(); + } + + @Override + public T next() { + return backing.next(); + } + + @Override + public void remove() { + backing.remove(); + } + + @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) { + int index = indexOf(o); + if (index < 0) return false; + T old = backing.remove(index); + assert old == o; + return old != null; + } + + @Override + public boolean removeAll(Collection c) { + return backing.removeIf(c::contains); + } + + @Override + public boolean retainAll(Collection c) { + return backing.removeIf((o) -> !c.contains(o)); + } + + @Override + public boolean addAll(Collection c) { + if (containsAll(c)) return false; + backing.addAll(c); + this.sort(); + return true; + } + + @Override + public void clear() { + backing.clear(); + } + + @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) { + return backing.removeIf(filter); + } +} 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 849ba1cf..25526320 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 int 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 00000000..5aa306a1 --- /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.21.0