Skip to content

Commit 62287ac

Browse files
committed
Benchmark primitive culling against production grid
1 parent eb07a29 commit 62287ac

4 files changed

Lines changed: 130 additions & 136 deletions

File tree

benchmark/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemBenchmarkPlugin.java

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import java.nio.file.Path;
2727
import java.util.ArrayList;
2828
import java.util.Arrays;
29+
import java.util.HashMap;
2930
import java.util.List;
3031
import java.util.Locale;
32+
import java.util.Map;
3133
import java.util.Random;
3234
import java.util.UUID;
3335
import java.util.function.LongSupplier;
@@ -186,34 +188,54 @@ private void benchmarkVisibility(int itemCount, int viewerCount, String distribu
186188
}
187189
default -> throw new IllegalArgumentException("Unknown visibility distribution: " + distribution);
188190
}
189-
LongSupplier baseline = () -> linearActiveLabels(items, viewers, VIEW_DISTANCE);
191+
LongSupplier linearReference = () -> linearActiveLabels(items, viewers, VIEW_DISTANCE);
192+
LongSupplier legacyProduction = () -> legacyGridActiveLabels(items, viewers, VIEW_DISTANCE);
190193
LongSupplier candidate = () -> indexedActiveLabels(items, viewers, VIEW_DISTANCE);
191-
long expectedLabels = baseline.getAsLong();
194+
long expectedLabels = linearReference.getAsLong();
195+
long legacyLabels = legacyProduction.getAsLong();
192196
long candidateLabels = candidate.getAsLong();
193-
if (expectedLabels != candidateLabels) {
194-
throw new IllegalStateException("Visibility A/B mismatch: " + expectedLabels + " != " + candidateLabels);
197+
if (expectedLabels != legacyLabels || expectedLabels != candidateLabels) {
198+
throw new IllegalStateException("Visibility A/B mismatch: linear=" + expectedLabels
199+
+ ", legacy=" + legacyLabels + ", candidate=" + candidateLabels);
200+
}
201+
Comparison productionComparison;
202+
Comparison referenceComparison;
203+
if (((itemCount + viewerCount + seed) & 1) == 0) {
204+
productionComparison = compare(legacyProduction, candidate);
205+
referenceComparison = compare(linearReference, candidate);
206+
} else {
207+
referenceComparison = compare(linearReference, candidate);
208+
productionComparison = compare(legacyProduction, candidate);
195209
}
196-
String candidateStorage = candidateStorage(itemCount, viewers);
197-
Comparison comparison = compare(baseline, candidate);
198210
double reduction = itemCount == 0 ? 0.0D : 100.0D * (itemCount - expectedLabels) / itemCount;
211+
reportVisibilityComparison("visibility-production-ab", "legacy-production-viewer-grid", true,
212+
distribution, seed, itemCount, viewerCount, productionComparison, expectedLabels, reduction);
213+
reportVisibilityComparison("visibility-linear-reference", "linear-viewer-snapshot-any", false,
214+
distribution, seed, itemCount, viewerCount, referenceComparison, expectedLabels, reduction);
215+
}
216+
217+
private void reportVisibilityComparison(String benchmark, String baseline, boolean baselineUsesGrid,
218+
String distribution, int seed, int itemCount, int viewerCount,
219+
Comparison comparison, long activeLabels, double reduction) {
199220
String result = String.format(Locale.ROOT,
200-
"{\"benchmark\":\"visibility\",\"distribution\":\"%s\",\"seed\":%d," +
221+
"{\"benchmark\":\"%s\",\"distribution\":\"%s\",\"seed\":%d," +
201222
"\"items\":%d,\"viewers\":%d," +
202-
"\"range\":%.1f,\"baseline\":\"linear-viewer-snapshot-any\"," +
203-
"\"candidate\":\"production-hybrid-viewer-index\",\"candidateStorage\":\"%s\"," +
223+
"\"range\":%.1f,\"baseline\":\"%s\",\"baselineUsesGrid\":%b," +
224+
"\"candidate\":\"production-primitive-viewer-index\"," +
225+
"\"candidateStorage\":\"primitive-soa\"," +
204226
"\"indexRebuiltPerOperation\":true,\"candidateUsesGrid\":false," +
205227
"\"sampleTargetNs\":%d,\"measurementRounds\":%d," +
206228
"\"baselineMedianNs\":%d,\"baselineP95Ns\":%d," +
207229
"\"candidateMedianNs\":%d,\"candidateP95Ns\":%d,\"speedup\":%.3f," +
208230
"\"baselineRoundsNs\":%s,\"candidateRoundsNs\":%s," +
209231
"\"allLabels\":%d,\"activeLabels\":%d," +
210232
"\"labelReductionPct\":%.3f}",
211-
distribution, seed, itemCount, viewerCount, VIEW_DISTANCE, candidateStorage,
233+
benchmark, distribution, seed, itemCount, viewerCount, VIEW_DISTANCE, baseline, baselineUsesGrid,
212234
SAMPLE_TARGET_NANOS, MEASUREMENT_ROUNDS,
213235
comparison.baseline().median(), comparison.baseline().p95(),
214236
comparison.candidate().median(), comparison.candidate().p95(), comparison.speedup(),
215237
Arrays.toString(comparison.baselineRoundsNs()), Arrays.toString(comparison.candidateRoundsNs()),
216-
itemCount, expectedLabels, reduction);
238+
itemCount, activeLabels, reduction);
217239
report(result);
218240
}
219241

@@ -245,8 +267,11 @@ private static long linearActiveLabels(List<Point> items, List<Point> viewers, d
245267
return active;
246268
}
247269

248-
private static long indexedActiveLabels(List<Point> items, List<Point> viewers, double range) {
249-
DroppedItemSpatialIndex.ViewerIndex index = createViewerIndex(viewers, items.size());
270+
private static long legacyGridActiveLabels(List<Point> items, List<Point> viewers, double range) {
271+
LegacyViewerIndex index = new LegacyViewerIndex();
272+
for (Point viewer : viewers) {
273+
index.addViewer(BENCHMARK_WORLD_ID, viewer.x(), viewer.y(), viewer.z());
274+
}
250275
long active = 0;
251276
for (Point item : items) {
252277
if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range)) {
@@ -257,13 +282,20 @@ private static long indexedActiveLabels(List<Point> items, List<Point> viewers,
257282
return active;
258283
}
259284

260-
private static String candidateStorage(int itemCount, List<Point> viewers) {
261-
return createViewerIndex(viewers, itemCount).usesPointStorage() ? "point-array" : "primitive-soa";
285+
private static long indexedActiveLabels(List<Point> items, List<Point> viewers, double range) {
286+
DroppedItemSpatialIndex.ViewerIndex index = createViewerIndex(viewers);
287+
long active = 0;
288+
for (Point item : items) {
289+
if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range)) {
290+
active++;
291+
}
292+
}
293+
blackhole = active;
294+
return active;
262295
}
263296

264-
private static DroppedItemSpatialIndex.ViewerIndex createViewerIndex(List<Point> viewers, int itemCount) {
265-
DroppedItemSpatialIndex.ViewerIndex index =
266-
new DroppedItemSpatialIndex.ViewerIndex(viewers.size(), itemCount);
297+
private static DroppedItemSpatialIndex.ViewerIndex createViewerIndex(List<Point> viewers) {
298+
DroppedItemSpatialIndex.ViewerIndex index = new DroppedItemSpatialIndex.ViewerIndex(viewers.size());
267299
for (Point viewer : viewers) {
268300
index.addViewer(BENCHMARK_WORLD_ID, viewer.x(), viewer.y(), viewer.z());
269301
}
@@ -339,6 +371,60 @@ private record Comparison(Samples baseline, Samples candidate, long[] baselineRo
339371
long[] candidateRoundsNs, double speedup) {
340372
}
341373

374+
/** Exact copy of the viewer-grid strategy used by production before the optimized candidate. */
375+
private static final class LegacyViewerIndex {
376+
377+
private static final double CELL_SIZE = 16.0D;
378+
private final Map<LegacyViewerCell, List<LegacyViewerPoint>> viewerCells = new HashMap<>();
379+
380+
private void addViewer(UUID worldId, double x, double y, double z) {
381+
viewerCells.computeIfAbsent(cell(worldId, x, z), ignored -> new ArrayList<>())
382+
.add(new LegacyViewerPoint(x, y, z));
383+
}
384+
385+
private boolean hasViewerWithin(UUID worldId, double x, double y, double z, double range) {
386+
if (range < 0.0D) {
387+
return false;
388+
}
389+
int minimumX = coordinate(x - range);
390+
int maximumX = coordinate(x + range);
391+
int minimumZ = coordinate(z - range);
392+
int maximumZ = coordinate(z + range);
393+
double rangeSquared = range * range;
394+
for (int cellX = minimumX; cellX <= maximumX; cellX++) {
395+
for (int cellZ = minimumZ; cellZ <= maximumZ; cellZ++) {
396+
List<LegacyViewerPoint> points = viewerCells.get(new LegacyViewerCell(worldId, cellX, cellZ));
397+
if (points == null) {
398+
continue;
399+
}
400+
for (LegacyViewerPoint point : points) {
401+
double deltaX = point.x() - x;
402+
double deltaY = point.y() - y;
403+
double deltaZ = point.z() - z;
404+
if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= rangeSquared) {
405+
return true;
406+
}
407+
}
408+
}
409+
}
410+
return false;
411+
}
412+
413+
private static LegacyViewerCell cell(UUID worldId, double x, double z) {
414+
return new LegacyViewerCell(worldId, coordinate(x), coordinate(z));
415+
}
416+
417+
private static int coordinate(double value) {
418+
return (int) Math.floor(value / CELL_SIZE);
419+
}
420+
}
421+
422+
private record LegacyViewerCell(UUID worldId, int x, int z) {
423+
}
424+
425+
private record LegacyViewerPoint(double x, double y, double z) {
426+
}
427+
342428
private record Point(int id, double x, double y, double z) {
343429

344430
private double distanceSquared(Point other) {

common/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemDisplay.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ private void tickAll() {
243243

244244
private void tickAllInternal() {
245245
Collection<Player> viewers = reconcileEligibleViewers();
246+
DroppedItemSpatialIndex.ViewerIndex viewerIndex =
247+
new DroppedItemSpatialIndex.ViewerIndex(viewers.size());
248+
for (Player viewer : viewers) {
249+
Location location = viewer.getLocation();
250+
viewerIndex.addViewer(viewer.getWorld().getUID(), location.getX(), location.getY(), location.getZ());
251+
}
252+
246253
List<TrackedItem> validItems = new ArrayList<>(trackedItems.size());
247254
Iterator<Map.Entry<UUID, Item>> iterator = trackedItems.entrySet().iterator();
248255
while (iterator.hasNext()) {
@@ -256,13 +263,6 @@ private void tickAllInternal() {
256263
validItems.add(new TrackedItem(entry.getKey(), item, item.getLocation()));
257264
}
258265

259-
DroppedItemSpatialIndex.ViewerIndex viewerIndex =
260-
new DroppedItemSpatialIndex.ViewerIndex(viewers.size(), validItems.size());
261-
for (Player viewer : viewers) {
262-
Location location = viewer.getLocation();
263-
viewerIndex.addViewer(viewer.getWorld().getUID(), location.getX(), location.getY(), location.getZ());
264-
}
265-
266266
if (viewerIndex.isEmpty()) {
267267
for (UUID itemId : new HashSet<>(labels.keySet())) {
268268
removeLabel(itemId);

common/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemSpatialIndex.java

Lines changed: 18 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,10 @@ private static int itemCoordinate(double coordinate) {
7474

7575
static final class ViewerIndex {
7676

77-
private static final int POINT_STORAGE_MAX_ITEMS_PER_VIEWER = 4;
78-
7977
private UUID singleWorldId;
8078
private WorldViewerBucket singleWorldViewers;
8179
private Map<UUID, WorldViewerBucket> viewersByWorld;
8280
private final int expectedViewers;
83-
private final boolean pointStorage;
8481

8582
ViewerIndex() {
8683
this(0);
@@ -91,25 +88,12 @@ static final class ViewerIndex {
9188
throw new IllegalArgumentException("expectedViewers must be non-negative");
9289
}
9390
this.expectedViewers = expectedViewers;
94-
this.pointStorage = false;
95-
}
96-
97-
ViewerIndex(int expectedViewers, int expectedItems) {
98-
if (expectedViewers < 0) {
99-
throw new IllegalArgumentException("expectedViewers must be non-negative");
100-
}
101-
if (expectedItems < 0) {
102-
throw new IllegalArgumentException("expectedItems must be non-negative");
103-
}
104-
this.expectedViewers = expectedViewers;
105-
this.pointStorage = expectedViewers > 0
106-
&& (long) expectedItems <= (long) expectedViewers * POINT_STORAGE_MAX_ITEMS_PER_VIEWER;
10791
}
10892

10993
void addViewer(UUID worldId, double x, double y, double z) {
11094
if (singleWorldViewers == null) {
11195
singleWorldId = worldId;
112-
singleWorldViewers = createBucket(expectedViewers);
96+
singleWorldViewers = new WorldViewerBucket(expectedViewers);
11397
singleWorldViewers.add(x, y, z);
11498
return;
11599
}
@@ -121,23 +105,13 @@ void addViewer(UUID worldId, double x, double y, double z) {
121105
viewersByWorld = new HashMap<>();
122106
viewersByWorld.put(singleWorldId, singleWorldViewers);
123107
}
124-
viewersByWorld.computeIfAbsent(worldId, ignored -> createBucket(0)).add(x, y, z);
108+
viewersByWorld.computeIfAbsent(worldId, ignored -> new WorldViewerBucket(0)).add(x, y, z);
125109
}
126110

127111
boolean isEmpty() {
128112
return singleWorldViewers == null;
129113
}
130114

131-
boolean usesPointStorage() {
132-
return pointStorage;
133-
}
134-
135-
private WorldViewerBucket createBucket(int initialCapacity) {
136-
return pointStorage
137-
? new PointWorldViewerBucket(initialCapacity)
138-
: new PrimitiveWorldViewerBucket(initialCapacity);
139-
}
140-
141115
boolean hasViewerWithin(UUID worldId, double x, double y, double z, double range) {
142116
if (range < 0.0D) {
143117
return false;
@@ -148,32 +122,26 @@ boolean hasViewerWithin(UUID worldId, double x, double y, double z, double range
148122
return viewers != null && viewers.hasViewerWithin(x, y, z, range * range);
149123
}
150124

151-
private abstract static class WorldViewerBucket {
152-
153-
abstract void add(double x, double y, double z);
154-
155-
abstract boolean hasViewerWithin(double x, double y, double z, double rangeSquared);
156-
}
157-
158-
private static final class PrimitiveWorldViewerBucket extends WorldViewerBucket {
125+
private static final class WorldViewerBucket {
159126

127+
private static final int INITIAL_CAPACITY = 8;
128+
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
160129
private static final double[] EMPTY = new double[0];
161130

162131
private double[] xCoordinates = EMPTY;
163132
private double[] yCoordinates = EMPTY;
164133
private double[] zCoordinates = EMPTY;
165134
private int size;
166135

167-
private PrimitiveWorldViewerBucket(int initialCapacity) {
136+
private WorldViewerBucket(int initialCapacity) {
168137
if (initialCapacity > 0) {
169138
xCoordinates = new double[initialCapacity];
170139
yCoordinates = new double[initialCapacity];
171140
zCoordinates = new double[initialCapacity];
172141
}
173142
}
174143

175-
@Override
176-
void add(double x, double y, double z) {
144+
private void add(double x, double y, double z) {
177145
if (size == xCoordinates.length) {
178146
int capacity = grownCapacity(size);
179147
xCoordinates = grow(xCoordinates, capacity);
@@ -186,8 +154,7 @@ void add(double x, double y, double z) {
186154
size++;
187155
}
188156

189-
@Override
190-
boolean hasViewerWithin(double x, double y, double z, double rangeSquared) {
157+
private boolean hasViewerWithin(double x, double y, double z, double rangeSquared) {
191158
for (int index = 0; index < size; index++) {
192159
double deltaX = xCoordinates[index] - x;
193160
double deltaY = yCoordinates[index] - y;
@@ -198,66 +165,21 @@ boolean hasViewerWithin(double x, double y, double z, double rangeSquared) {
198165
}
199166
return false;
200167
}
201-
}
202-
203-
private static final class PointWorldViewerBucket extends WorldViewerBucket {
204168

205-
private static final Point[] EMPTY = new Point[0];
206-
207-
private Point[] points = EMPTY;
208-
private int size;
209-
210-
private PointWorldViewerBucket(int initialCapacity) {
211-
if (initialCapacity > 0) {
212-
points = new Point[initialCapacity];
169+
private static int grownCapacity(int size) {
170+
if (size >= MAX_ARRAY_SIZE) {
171+
throw new OutOfMemoryError("Too many viewers");
213172
}
173+
return size > MAX_ARRAY_SIZE / 2
174+
? MAX_ARRAY_SIZE
175+
: Math.max(INITIAL_CAPACITY, size << 1);
214176
}
215177

216-
@Override
217-
void add(double x, double y, double z) {
218-
if (size == points.length) {
219-
points = grow(points, grownCapacity(size));
220-
}
221-
points[size++] = new Point(x, y, z);
178+
private static double[] grow(double[] source, int capacity) {
179+
double[] expanded = new double[capacity];
180+
System.arraycopy(source, 0, expanded, 0, source.length);
181+
return expanded;
222182
}
223-
224-
@Override
225-
boolean hasViewerWithin(double x, double y, double z, double rangeSquared) {
226-
for (int index = 0; index < size; index++) {
227-
Point point = points[index];
228-
double deltaX = point.x() - x;
229-
double deltaY = point.y() - y;
230-
double deltaZ = point.z() - z;
231-
if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= rangeSquared) {
232-
return true;
233-
}
234-
}
235-
return false;
236-
}
237-
}
238-
239-
private static final int INITIAL_BUCKET_CAPACITY = 8;
240-
private static final int MAX_BUCKET_ARRAY_SIZE = Integer.MAX_VALUE - 8;
241-
242-
private static int grownCapacity(int size) {
243-
if (size >= MAX_BUCKET_ARRAY_SIZE) {
244-
throw new OutOfMemoryError("Too many viewers");
245-
}
246-
return size > MAX_BUCKET_ARRAY_SIZE / 2
247-
? MAX_BUCKET_ARRAY_SIZE
248-
: Math.max(INITIAL_BUCKET_CAPACITY, size << 1);
249-
}
250-
251-
private static double[] grow(double[] source, int capacity) {
252-
double[] expanded = new double[capacity];
253-
System.arraycopy(source, 0, expanded, 0, source.length);
254-
return expanded;
255-
}
256-
257-
private static Point[] grow(Point[] source, int capacity) {
258-
Point[] expanded = new Point[capacity];
259-
System.arraycopy(source, 0, expanded, 0, source.length);
260-
return expanded;
261183
}
262184
}
263185

0 commit comments

Comments
 (0)