Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ensure-labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
{ name: "performance-snapshot", color: "bfdadc", description: "Use the viewer snapshot performance profile" },
{ name: "performance-gb-bot", color: "bfdadc", description: "Use the GB bot decision performance profile" },
{ name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" },
{ name: "performance-display-spawn", color: "bfdadc", description: "Use the display entity spawn performance profile" },
{ name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" }
];

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/performance-ab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on:
- snapshot
- gb-bot
- region-fingerprint
- display-spawn
- ray-proxy
- infra
forks:
Expand Down Expand Up @@ -159,6 +160,7 @@ jobs:
"performance-snapshot": "snapshot",
"performance-gb-bot": "gb-bot",
"performance-region-fingerprint": "region-fingerprint",
"performance-display-spawn": "display-spawn",
"performance-ray-proxy": "ray-proxy",
}
selected = [profile for label, profile in choices.items() if label in labels]
Expand Down
26 changes: 26 additions & 0 deletions perf/ab/gate-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@
"top/ellan/mahjong/table/render/TableRegionFingerprintService.class"
]
},
"display-spawn": {
"include": "^top\\.ellan\\.mahjong\\.perf\\.DisplayEntitiesSpawnAllBenchmark\\.spawnRepresentativeTableRegions$",
"benchmark_ids": [
"display.spawn.time",
"display.spawn.alloc"
],
"minimum_passes": 2,
"must_pass": [
"display.spawn.time"
],
"required_classes": [
"top/ellan/mahjong/render/display/DisplayEntities.class"
]
},
"ray-proxy": {
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
"benchmark_ids": [
Expand Down Expand Up @@ -239,6 +253,18 @@
"metric": "gc.alloc.rate.norm",
"direction": "lower"
},
{
"id": "display.spawn.time",
"benchmark": "top.ellan.mahjong.perf.DisplayEntitiesSpawnAllBenchmark.spawnRepresentativeTableRegions",
"metric": "primary",
"direction": "lower"
},
{
"id": "display.spawn.alloc",
"benchmark": "top.ellan.mahjong.perf.DisplayEntitiesSpawnAllBenchmark.spawnRepresentativeTableRegions",
"metric": "gc.alloc.rate.norm",
"direction": "lower"
},
{
"id": "ray.viewers1.time",
"benchmark": "top.ellan.mahjong.perf.RayProxyCoordinatorBenchmark.proxyPlan1Viewer",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package top.ellan.mahjong.perf;

import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import top.ellan.mahjong.render.display.DisplayEntities;
import top.ellan.mahjong.render.display.DisplayEntityRuntime;

/** Measures the one-entity region shape used by hand, discard, meld and wall tiles. */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class DisplayEntitiesSpawnAllBenchmark {
private static final int REPRESENTATIVE_SINGLETON_REGION_COUNT = 268;

private Entity entity;
private DisplayEntityRuntime runtime;
private List<DisplayEntities.EntitySpec> singletonSpec;

@Setup(Level.Trial)
public void setUp() {
Plugin plugin = proxy(Plugin.class);
this.entity = proxy(Entity.class);
this.runtime = new EmptyViewerRuntime(plugin);
this.singletonSpec = List.of(new ConstantEntitySpec(this.entity));
this.verifyContract();
}

@Benchmark
public int spawnRepresentativeTableRegions() {
int spawned = 0;
for (int region = 0; region < REPRESENTATIVE_SINGLETON_REGION_COUNT; region++) {
spawned += DisplayEntities.spawnAll(this.runtime, this.singletonSpec).size();
}
return spawned;
}

private void verifyContract() {
List<Entity> spawned = DisplayEntities.spawnAll(this.runtime, this.singletonSpec);
if (spawned.size() != 1 || spawned.get(0) != this.entity) {
throw new IllegalStateException("Singleton spawn contract changed");
}
try {
spawned.add(this.entity);
throw new IllegalStateException("spawnAll must return an immutable list");
} catch (UnsupportedOperationException expected) {
// Expected contract.
}
}

@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type) {
return (T) Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[] {type},
(instance, method, arguments) -> {
throw new UnsupportedOperationException(method.toString());
}
);
}

private record EmptyViewerRuntime(Plugin bukkitPlugin) implements DisplayEntityRuntime {
@Override
public Collection<? extends Player> onlinePlayers() {
return List.of();
}
}

private record ConstantEntitySpec(Entity entity) implements DisplayEntities.EntitySpec {
@Override
public Entity spawn(DisplayEntityRuntime runtime) {
return this.entity;
}

@Override
public boolean canReuse(DisplayEntityRuntime runtime, Entity candidate) {
return candidate == this.entity;
}

@Override
public void apply(DisplayEntityRuntime runtime, Entity candidate) {
throw new UnsupportedOperationException("Benchmark does not reconcile entities");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package top.ellan.mahjong.render.display;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.Test;

class DisplayEntitiesSpawnAllContractTest {
@Test
void singletonSpawnReturnsTheSameEntityInAnImmutableList() {
Entity entity = proxy(Entity.class);
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));

List<Entity> spawned = DisplayEntities.spawnAll(runtime, List.of(new ConstantEntitySpec(entity)));

assertEquals(1, spawned.size());
assertSame(entity, spawned.get(0));
assertEquals(1, runtime.onlinePlayerReads());
assertThrows(UnsupportedOperationException.class, () -> spawned.add(entity));
}

@Test
void singletonNullSpawnReturnsAnImmutableEmptyList() {
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));

List<Entity> spawned = DisplayEntities.spawnAll(runtime, List.of(new ConstantEntitySpec(null)));

assertEquals(0, spawned.size());
assertEquals(1, runtime.onlinePlayerReads());
assertThrows(UnsupportedOperationException.class, () -> spawned.add(proxy(Entity.class)));
}

@Test
void multipleSpawnsPreserveOrderAndSkipNulls() {
Entity first = proxy(Entity.class);
Entity second = proxy(Entity.class);
CountingRuntime runtime = new CountingRuntime(proxy(Plugin.class));

List<Entity> spawned = DisplayEntities.spawnAll(
runtime,
List.of(new ConstantEntitySpec(first), new ConstantEntitySpec(null), new ConstantEntitySpec(second))
);

assertEquals(2, spawned.size());
assertSame(first, spawned.get(0));
assertSame(second, spawned.get(1));
assertEquals(1, runtime.onlinePlayerReads());
}

@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type) {
return (T) Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[] {type},
(instance, method, arguments) -> {
throw new UnsupportedOperationException(method.toString());
}
);
}

private static final class CountingRuntime implements DisplayEntityRuntime {
private final Plugin plugin;
private int onlinePlayerReads;

private CountingRuntime(Plugin plugin) {
this.plugin = plugin;
}

@Override
public Plugin bukkitPlugin() {
return this.plugin;
}

@Override
public Collection<? extends Player> onlinePlayers() {
this.onlinePlayerReads++;
return List.of();
}

private int onlinePlayerReads() {
return this.onlinePlayerReads;
}
}

private record ConstantEntitySpec(Entity entity) implements DisplayEntities.EntitySpec {
@Override
public Entity spawn(DisplayEntityRuntime runtime) {
return this.entity;
}

@Override
public boolean canReuse(DisplayEntityRuntime runtime, Entity candidate) {
return candidate == this.entity;
}

@Override
public void apply(DisplayEntityRuntime runtime, Entity candidate) {
throw new UnsupportedOperationException("Contract test does not reconcile entities");
}
}
}