Skip to content

Commit 6c7e414

Browse files
authored
Optimise WorldStatisticsProvider regionising (#506)
1 parent eefa144 commit 6c7e414

File tree

2 files changed

+210
-39
lines changed

2 files changed

+210
-39
lines changed

spark-common/src/main/java/me/lucko/spark/common/platform/world/WorldStatisticsProvider.java

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
package me.lucko.spark.common.platform.world;
2222

2323
import me.lucko.spark.proto.SparkProtos.WorldStatistics;
24+
import org.jetbrains.annotations.VisibleForTesting;
2425

26+
import java.util.ArrayDeque;
2527
import java.util.ArrayList;
2628
import java.util.Collection;
2729
import java.util.HashMap;
2830
import java.util.HashSet;
29-
import java.util.Iterator;
31+
import java.util.LinkedHashMap;
3032
import java.util.List;
33+
import java.util.Map;
3134
import java.util.Set;
3235
import java.util.concurrent.atomic.AtomicInteger;
3336

@@ -122,37 +125,57 @@ private static <E> WorldStatistics.Chunk chunkToProto(ChunkInfo<E> chunk, CountM
122125
return builder.build();
123126
}
124127

125-
private static List<Region> groupIntoRegions(List<? extends ChunkInfo<?>> chunks) {
128+
@VisibleForTesting
129+
static List<Region> groupIntoRegions(List<? extends ChunkInfo<?>> chunks) {
126130
List<Region> regions = new ArrayList<>();
127131

132+
LinkedHashMap<ChunkCoordinate, ChunkInfo<?>> chunkMap = new LinkedHashMap<>(chunks.size());
133+
128134
for (ChunkInfo<?> chunk : chunks) {
129135
CountMap<?> counts = chunk.getEntityCounts();
130136
if (counts.total().get() == 0) {
131137
continue;
132138
}
139+
chunkMap.put(new ChunkCoordinate(chunk.getX(), chunk.getZ()), chunk);
140+
}
133141

134-
boolean found = false;
142+
ArrayDeque<ChunkInfo<?>> queue = new ArrayDeque<>();
143+
ChunkCoordinate index = new ChunkCoordinate(); // avoid allocating per check
135144

136-
for (Region region : regions) {
137-
if (region.isAdjacent(chunk)) {
138-
found = true;
139-
region.add(chunk);
140-
141-
// if the chunk is adjacent to more than one region, merge the regions together
142-
for (Iterator<Region> iterator = regions.iterator(); iterator.hasNext(); ) {
143-
Region otherRegion = iterator.next();
144-
if (region != otherRegion && otherRegion.isAdjacent(chunk)) {
145-
iterator.remove();
146-
region.merge(otherRegion);
145+
while (!chunkMap.isEmpty()) {
146+
Map.Entry<ChunkCoordinate, ChunkInfo<?>> first = chunkMap.entrySet().iterator().next();
147+
ChunkInfo<?> firstValue = first.getValue();
148+
149+
chunkMap.remove(first.getKey());
150+
151+
Region region = new Region(firstValue);
152+
regions.add(region);
153+
154+
queue.add(firstValue);
155+
156+
ChunkInfo<?> queued;
157+
while ((queued = queue.pollFirst()) != null) {
158+
int queuedX = queued.getX();
159+
int queuedZ = queued.getZ();
160+
161+
// merge adjacent chunks
162+
for (int dz = -1; dz <= 1; ++dz) {
163+
for (int dx = -1; dx <= 1; ++dx) {
164+
if ((dx | dz) == 0) {
165+
continue;
147166
}
148-
}
149167

150-
break;
151-
}
152-
}
168+
index.setCoordinate(queuedX + dx, queuedZ + dz);
169+
ChunkInfo<?> adjacent = chunkMap.remove(index);
170+
171+
if (adjacent == null) {
172+
continue;
173+
}
153174

154-
if (!found) {
155-
regions.add(new Region(chunk));
175+
region.add(adjacent);
176+
queue.add(adjacent);
177+
}
178+
}
156179
}
157180
}
158181

@@ -162,8 +185,7 @@ private static List<Region> groupIntoRegions(List<? extends ChunkInfo<?>> chunks
162185
/**
163186
* A map of nearby chunks grouped together by Euclidean distance.
164187
*/
165-
private static final class Region {
166-
private static final int DISTANCE_THRESHOLD = 2;
188+
static final class Region {
167189
private final Set<ChunkInfo<?>> chunks;
168190
private final AtomicInteger totalEntities;
169191

@@ -181,30 +203,53 @@ public AtomicInteger getTotalEntities() {
181203
return this.totalEntities;
182204
}
183205

184-
public boolean isAdjacent(ChunkInfo<?> chunk) {
185-
for (ChunkInfo<?> el : this.chunks) {
186-
if (squaredEuclideanDistance(el, chunk) <= DISTANCE_THRESHOLD) {
187-
return true;
188-
}
189-
}
190-
return false;
191-
}
192-
193206
public void add(ChunkInfo<?> chunk) {
194207
this.chunks.add(chunk);
195208
this.totalEntities.addAndGet(chunk.getEntityCounts().total().get());
196209
}
210+
}
211+
212+
static final class ChunkCoordinate implements Comparable<ChunkCoordinate> {
213+
long key;
214+
215+
ChunkCoordinate() {}
197216

198-
public void merge(Region group) {
199-
this.chunks.addAll(group.getChunks());
200-
this.totalEntities.addAndGet(group.getTotalEntities().get());
217+
ChunkCoordinate(int chunkX, int chunkZ) {
218+
this.setCoordinate(chunkX, chunkZ);
201219
}
202220

203-
private static long squaredEuclideanDistance(ChunkInfo<?> a, ChunkInfo<?> b) {
204-
long dx = a.getX() - b.getX();
205-
long dz = a.getZ() - b.getZ();
206-
return (dx * dx) + (dz * dz);
221+
ChunkCoordinate(long key) {
222+
this.setKey(key);
223+
}
224+
225+
public void setCoordinate(int chunkX, int chunkZ) {
226+
this.setKey(((long) chunkZ << 32) | (chunkX & 0xFFFFFFFFL));
207227
}
208-
}
209228

229+
public void setKey(long key) {
230+
this.key = key;
231+
}
232+
233+
@Override
234+
public int hashCode() {
235+
// fastutil hash without the last step, as it is done by HashMap
236+
// doing the last step twice (h ^= (h >>> 16)) is both more expensive and destroys the hash
237+
long h = this.key * 0x9E3779B97F4A7C15L;
238+
h ^= h >>> 32;
239+
return (int) h;
240+
}
241+
242+
@Override
243+
public boolean equals(Object obj) {
244+
if (!(obj instanceof ChunkCoordinate)) {
245+
return false;
246+
}
247+
return this.key == ((ChunkCoordinate) obj).key;
248+
}
249+
250+
@Override
251+
public int compareTo(ChunkCoordinate other) {
252+
return Long.compare(this.key, other.key);
253+
}
254+
}
210255
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* This file is part of spark.
3+
*
4+
* Copyright (c) lucko (Luck) <[email protected]>
5+
* Copyright (c) contributors
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package me.lucko.spark.common.platform.world;
22+
23+
import com.google.common.collect.ImmutableList;
24+
import com.google.common.collect.ImmutableSet;
25+
import org.junit.jupiter.api.Test;
26+
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Set;
30+
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
33+
public class WorldStatisticsProviderTest {
34+
35+
@Test
36+
public void testGroupIntoRegionsEmpty() {
37+
List<WorldStatisticsProvider.Region> regions = WorldStatisticsProvider.groupIntoRegions(ImmutableList.of());
38+
assertEquals(0, regions.size());
39+
}
40+
41+
@Test
42+
public void testGroupIntoRegionsSingle() {
43+
TestChunkInfo chunk1 = new TestChunkInfo(0, 0);
44+
List<WorldStatisticsProvider.Region> regions = WorldStatisticsProvider.groupIntoRegions(ImmutableList.of(chunk1));
45+
46+
assertEquals(1, regions.size());
47+
WorldStatisticsProvider.Region region = regions.get(0);
48+
49+
Set<ChunkInfo<?>> chunks = region.getChunks();
50+
assertEquals(1, chunks.size());
51+
assertEquals(ImmutableSet.of(chunk1), chunks);
52+
}
53+
54+
@Test
55+
public void testGroupIntoRegionsMultiple() {
56+
TestChunkInfo chunk1 = new TestChunkInfo(0, 0);
57+
TestChunkInfo chunk2 = new TestChunkInfo(0, 1);
58+
TestChunkInfo chunk3 = new TestChunkInfo(1, 0);
59+
TestChunkInfo chunk4 = new TestChunkInfo(0, 2);
60+
61+
List<WorldStatisticsProvider.Region> regions = WorldStatisticsProvider.groupIntoRegions(ImmutableList.of(chunk1, chunk2, chunk3, chunk4));
62+
63+
assertEquals(1, regions.size());
64+
65+
WorldStatisticsProvider.Region region = regions.get(0);
66+
Set<ChunkInfo<?>> chunks = region.getChunks();
67+
assertEquals(4, chunks.size());
68+
assertEquals(ImmutableSet.of(chunk1, chunk2, chunk3, chunk4), chunks);
69+
}
70+
71+
@Test
72+
public void testGroupIntoRegionsMultipleRegions() {
73+
TestChunkInfo chunk1 = new TestChunkInfo(0, 0);
74+
TestChunkInfo chunk2 = new TestChunkInfo(0, 1);
75+
TestChunkInfo chunk3 = new TestChunkInfo(1, 0);
76+
TestChunkInfo chunk4 = new TestChunkInfo(2, 2);
77+
78+
List<WorldStatisticsProvider.Region> regions = WorldStatisticsProvider.groupIntoRegions(ImmutableList.of(chunk1, chunk2, chunk3, chunk4));
79+
80+
assertEquals(2, regions.size());
81+
82+
WorldStatisticsProvider.Region region1 = regions.get(0);
83+
Set<ChunkInfo<?>> chunks1 = region1.getChunks();
84+
assertEquals(3, chunks1.size());
85+
assertEquals(ImmutableSet.of(chunk1, chunk2, chunk3), chunks1);
86+
87+
WorldStatisticsProvider.Region region2 = regions.get(1);
88+
Set<ChunkInfo<?>> chunks2 = region2.getChunks();
89+
assertEquals(1, chunks2.size());
90+
assertEquals(ImmutableSet.of(chunk4), chunks2);
91+
}
92+
93+
private static final class TestChunkInfo implements ChunkInfo<String> {
94+
private final int x;
95+
private final int z;
96+
private final CountMap<String> entityCounts;
97+
98+
public TestChunkInfo(int x, int z) {
99+
this.x = x;
100+
this.z = z;
101+
this.entityCounts = new CountMap.Simple<>(new HashMap<>());
102+
this.entityCounts.increment("test");
103+
}
104+
105+
@Override
106+
public int getX() {
107+
return this.x;
108+
}
109+
110+
@Override
111+
public int getZ() {
112+
return this.z;
113+
}
114+
115+
@Override
116+
public CountMap<String> getEntityCounts() {
117+
return this.entityCounts;
118+
}
119+
120+
@Override
121+
public String entityTypeName(String type) {
122+
return type;
123+
}
124+
}
125+
126+
}

0 commit comments

Comments
 (0)