Skip to content

Commit aac535e

Browse files
committed
perf: benchmark region update priority queue
1 parent f778f8b commit aac535e

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

.github/workflows/ensure-labels.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
{ name: "performance-gb-bot", color: "bfdadc", description: "Use the GB bot decision performance profile" },
2828
{ name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" },
2929
{ name: "performance-display-spawn", color: "bfdadc", description: "Use the display entity spawn performance profile" },
30+
{ name: "performance-region-queue", color: "bfdadc", description: "Use the region update queue performance profile" },
3031
{ name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" }
3132
];
3233

.github/workflows/performance-ab.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ on:
3535
- gb-bot
3636
- region-fingerprint
3737
- display-spawn
38+
- region-queue
3839
- ray-proxy
3940
- infra
4041
forks:
@@ -161,6 +162,7 @@ jobs:
161162
"performance-gb-bot": "gb-bot",
162163
"performance-region-fingerprint": "region-fingerprint",
163164
"performance-display-spawn": "display-spawn",
165+
"performance-region-queue": "region-queue",
164166
"performance-ray-proxy": "ray-proxy",
165167
}
166168
selected = [profile for label, profile in choices.items() if label in labels]

perf/ab/gate-config.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@
124124
"top/ellan/mahjong/render/display/DisplayEntities.class"
125125
]
126126
},
127+
"region-queue": {
128+
"include": "^top\\.ellan\\.mahjong\\.perf\\.RegionUpdateQueueBenchmark\\.orderRepresentativeTableRegions$",
129+
"benchmark_ids": [
130+
"region.queue.time",
131+
"region.queue.alloc"
132+
],
133+
"minimum_passes": 2,
134+
"must_pass": [
135+
"region.queue.time"
136+
],
137+
"required_classes": [
138+
"top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class"
139+
]
140+
},
127141
"ray-proxy": {
128142
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
129143
"benchmark_ids": [
@@ -265,6 +279,18 @@
265279
"metric": "gc.alloc.rate.norm",
266280
"direction": "lower"
267281
},
282+
{
283+
"id": "region.queue.time",
284+
"benchmark": "top.ellan.mahjong.perf.RegionUpdateQueueBenchmark.orderRepresentativeTableRegions",
285+
"metric": "primary",
286+
"direction": "lower"
287+
},
288+
{
289+
"id": "region.queue.alloc",
290+
"benchmark": "top.ellan.mahjong.perf.RegionUpdateQueueBenchmark.orderRepresentativeTableRegions",
291+
"metric": "gc.alloc.rate.norm",
292+
"direction": "lower"
293+
},
268294
{
269295
"id": "ray.viewers1.time",
270296
"benchmark": "top.ellan.mahjong.perf.RayProxyCoordinatorBenchmark.proxyPlan1Viewer",
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package top.ellan.mahjong.perf;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.invoke.MethodType;
6+
import java.lang.reflect.Constructor;
7+
import java.lang.reflect.Proxy;
8+
import java.util.ArrayList;
9+
import java.util.Comparator;
10+
import java.util.List;
11+
import java.util.concurrent.TimeUnit;
12+
import java.util.function.BooleanSupplier;
13+
import java.util.function.LongFunction;
14+
import org.openjdk.jmh.annotations.Benchmark;
15+
import org.openjdk.jmh.annotations.BenchmarkMode;
16+
import org.openjdk.jmh.annotations.Level;
17+
import org.openjdk.jmh.annotations.Mode;
18+
import org.openjdk.jmh.annotations.OutputTimeUnit;
19+
import org.openjdk.jmh.annotations.Scope;
20+
import org.openjdk.jmh.annotations.Setup;
21+
import org.openjdk.jmh.annotations.State;
22+
import top.ellan.mahjong.table.render.TableRegionDisplayCoordinator;
23+
24+
/** Measures the private priority queue used to apply a representative table render. */
25+
@State(Scope.Thread)
26+
@BenchmarkMode(Mode.AverageTime)
27+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
28+
public class RegionUpdateQueueBenchmark {
29+
private static final int PRIORITY_REACTION_PROMPT = 400;
30+
private static final int PRIORITY_HAND = 320;
31+
private static final int PRIORITY_TURN_STATE = 240;
32+
private static final int PRIORITY_BOARD = 160;
33+
private static final int PRIORITY_BACKGROUND = 80;
34+
private static final int WALL_TILE_REGIONS = 136;
35+
private static final int SEATS = 4;
36+
private static final int HAND_TILES_PER_SEAT = 11;
37+
private static final int DISCARDS_PER_SEAT = 18;
38+
private static final int MELD_TILES_PER_SEAT = 4;
39+
private static final int EXPECTED_REGION_UPDATES = 327;
40+
41+
private TableRegionDisplayCoordinator coordinator;
42+
private Class<?> actionType;
43+
private Constructor<?> updateConstructor;
44+
private MethodHandle applyQueue;
45+
private MethodHandle deferredAccessor;
46+
private MethodHandle processedUpdatesAccessor;
47+
private List<Integer> priorityPlan;
48+
private List<Object> template;
49+
private List<Object> working;
50+
51+
@Setup(Level.Trial)
52+
public void setUp() throws Throwable {
53+
this.coordinator = new TableRegionDisplayCoordinator(null, null);
54+
Class<?> coordinatorType = TableRegionDisplayCoordinator.class;
55+
Class<?> updateType = Class.forName(coordinatorType.getName() + "$QueuedRegionUpdate");
56+
Class<?> executionType = Class.forName(coordinatorType.getName() + "$QueueExecution");
57+
this.actionType = Class.forName(coordinatorType.getName() + "$RegionUpdateAction");
58+
59+
this.updateConstructor = updateType.getDeclaredConstructor(int.class, long.class, this.actionType);
60+
this.updateConstructor.setAccessible(true);
61+
62+
MethodHandles.Lookup coordinatorLookup = MethodHandles.privateLookupIn(coordinatorType, MethodHandles.lookup());
63+
this.applyQueue = coordinatorLookup.findVirtual(
64+
coordinatorType,
65+
"applyQueue",
66+
MethodType.methodType(executionType, List.class)
67+
);
68+
MethodHandles.Lookup executionLookup = MethodHandles.privateLookupIn(executionType, MethodHandles.lookup());
69+
this.deferredAccessor = executionLookup.findVirtual(
70+
executionType,
71+
"deferred",
72+
MethodType.methodType(boolean.class)
73+
);
74+
this.processedUpdatesAccessor = executionLookup.findVirtual(
75+
executionType,
76+
"processedUpdates",
77+
MethodType.methodType(int.class)
78+
);
79+
80+
this.priorityPlan = this.createPriorityPlan();
81+
this.verifyQueueContract();
82+
Object alwaysApply = this.action(() -> true);
83+
this.template = this.createUpdates(sequence -> alwaysApply);
84+
this.working = new ArrayList<>(this.template);
85+
}
86+
87+
@Setup(Level.Invocation)
88+
public void restoreProductionOrder() {
89+
for (int index = 0; index < this.template.size(); index++) {
90+
this.working.set(index, this.template.get(index));
91+
}
92+
}
93+
94+
@Benchmark
95+
public Object orderRepresentativeTableRegions() throws Throwable {
96+
return this.applyQueue.invoke(this.coordinator, this.working);
97+
}
98+
99+
private List<Integer> createPriorityPlan() {
100+
List<Integer> priorities = new ArrayList<>(EXPECTED_REGION_UPDATES);
101+
priorities.add(PRIORITY_BOARD);
102+
this.repeat(priorities, PRIORITY_BACKGROUND, WALL_TILE_REGIONS);
103+
priorities.add(PRIORITY_BOARD);
104+
priorities.add(PRIORITY_REACTION_PROMPT);
105+
for (int seat = 0; seat < SEATS; seat++) {
106+
priorities.add(PRIORITY_BACKGROUND);
107+
priorities.add(PRIORITY_REACTION_PROMPT);
108+
priorities.add(PRIORITY_TURN_STATE);
109+
this.repeat(priorities, PRIORITY_HAND, HAND_TILES_PER_SEAT);
110+
this.repeat(priorities, PRIORITY_HAND, HAND_TILES_PER_SEAT);
111+
this.repeat(priorities, PRIORITY_TURN_STATE, DISCARDS_PER_SEAT);
112+
this.repeat(priorities, PRIORITY_TURN_STATE, MELD_TILES_PER_SEAT);
113+
}
114+
if (priorities.size() != EXPECTED_REGION_UPDATES) {
115+
throw new IllegalStateException(
116+
"Representative region count changed: expected=" + EXPECTED_REGION_UPDATES + ", actual=" + priorities.size()
117+
);
118+
}
119+
return List.copyOf(priorities);
120+
}
121+
122+
private void repeat(List<Integer> priorities, int priority, int count) {
123+
for (int index = 0; index < count; index++) {
124+
priorities.add(priority);
125+
}
126+
}
127+
128+
private List<Object> createUpdates(LongFunction<Object> actionFactory) throws ReflectiveOperationException {
129+
List<Object> updates = new ArrayList<>(this.priorityPlan.size());
130+
for (int index = 0; index < this.priorityPlan.size(); index++) {
131+
long sequence = index;
132+
updates.add(this.updateConstructor.newInstance(
133+
this.priorityPlan.get(index),
134+
sequence,
135+
actionFactory.apply(sequence)
136+
));
137+
}
138+
return updates;
139+
}
140+
141+
private Object action(BooleanSupplier result) {
142+
return Proxy.newProxyInstance(
143+
this.actionType.getClassLoader(),
144+
new Class<?>[] {this.actionType},
145+
(proxy, method, arguments) -> {
146+
if (method.getName().equals("apply") && method.getParameterCount() == 0) {
147+
return result.getAsBoolean();
148+
}
149+
throw new UnsupportedOperationException(method.toString());
150+
}
151+
);
152+
}
153+
154+
private void verifyQueueContract() throws Throwable {
155+
List<Long> expectedOrder = new ArrayList<>(this.priorityPlan.size());
156+
for (int index = 0; index < this.priorityPlan.size(); index++) {
157+
expectedOrder.add((long) index);
158+
}
159+
expectedOrder.sort(
160+
Comparator.comparingInt((Long sequence) -> this.priorityPlan.get(sequence.intValue()))
161+
.reversed()
162+
.thenComparingLong(Long::longValue)
163+
);
164+
165+
List<Long> observedOrder = new ArrayList<>(this.priorityPlan.size());
166+
List<Object> updates = this.createUpdates(sequence -> this.action(() -> {
167+
observedOrder.add(sequence);
168+
return true;
169+
}));
170+
Object execution = this.applyQueue.invoke(this.coordinator, updates);
171+
this.assertExecution(execution, false, EXPECTED_REGION_UPDATES);
172+
if (!expectedOrder.equals(observedOrder)) {
173+
throw new IllegalStateException("Region update priority/sequence order changed");
174+
}
175+
176+
int failureIndex = EXPECTED_REGION_UPDATES / 2;
177+
long failureSequence = expectedOrder.get(failureIndex);
178+
observedOrder.clear();
179+
updates = this.createUpdates(sequence -> this.action(() -> {
180+
observedOrder.add(sequence);
181+
return sequence != failureSequence;
182+
}));
183+
execution = this.applyQueue.invoke(this.coordinator, updates);
184+
this.assertExecution(execution, true, failureIndex);
185+
if (!expectedOrder.subList(0, failureIndex + 1).equals(observedOrder)) {
186+
throw new IllegalStateException("Deferred region update boundary changed");
187+
}
188+
}
189+
190+
private void assertExecution(Object execution, boolean expectedDeferred, int expectedProcessed) throws Throwable {
191+
boolean deferred = (boolean) this.deferredAccessor.invoke(execution);
192+
int processed = (int) this.processedUpdatesAccessor.invoke(execution);
193+
if (deferred != expectedDeferred || processed != expectedProcessed) {
194+
throw new IllegalStateException(
195+
"Queue result changed: deferred=" + deferred + ", processed=" + processed
196+
);
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)