Skip to content

Commit 51f2614

Browse files
authored
Merge pull request #109 from EllanServer/codex/perf-hotspot-baselines
perf: add full hotspot lifecycle benchmarks
2 parents 39e4bf6 + 074e2fd commit 51f2614

7 files changed

Lines changed: 1094 additions & 0 deletions

File tree

.github/workflows/performance-ab.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ on:
4040
- layout
4141
- scheduler-reflection
4242
- ray-proxy
43+
- riichi-reaction
44+
- sichuan-hu
45+
- region-apply
46+
- layout-cache
4347
- infra
4448
forks:
4549
description: Trusted CI override for JMH forks (blank uses base config)
@@ -157,6 +161,10 @@ jobs:
157161
"performance-layout": "layout",
158162
"performance-scheduler-reflection": "scheduler-reflection",
159163
"performance-ray-proxy": "ray-proxy",
164+
"performance-riichi-reaction": "riichi-reaction",
165+
"performance-sichuan-hu": "sichuan-hu",
166+
"performance-region-apply": "region-apply",
167+
"performance-layout-cache": "layout-cache",
160168
}
161169
selected = [profile for label, profile in choices.items() if label in labels]
162170
if len(selected) != 1:

perf/ab/gate-config.json

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,88 @@
189189
"top/ellan/mahjong/runtime/ServerScheduler.class"
190190
]
191191
},
192+
"riichi-reaction": {
193+
"include": "^top\\.ellan\\.mahjong\\.riichi\\.RiichiReactionBenchmark\\.(?:structurallyImpossibleCall|structurallyImpossibleRonOnly|firstPossiblePonAnalysis)$",
194+
"benchmark_ids": [
195+
"riichi.impossible.time",
196+
"riichi.impossible.alloc",
197+
"riichi.ron-only.time",
198+
"riichi.ron-only.alloc",
199+
"riichi.pon.time",
200+
"riichi.pon.alloc"
201+
],
202+
"minimum_passes": 2,
203+
"must_pass": [
204+
"riichi.impossible.time",
205+
"riichi.ron-only.time"
206+
],
207+
"required_classes": [
208+
"top/ellan/mahjong/riichi/RiichiPlayerAnalysisState.class",
209+
"top/ellan/mahjong/riichi/RiichiPlayerState.class"
210+
]
211+
},
212+
"sichuan-hu": {
213+
"include": "^top\\.ellan\\.mahjong\\.table\\.core\\.round\\.SichuanHuEvaluatorBenchmark\\.(?:waitingTilesStandardHand|evaluateStandardWin|canWinWithFixedMeld)$",
214+
"benchmark_ids": [
215+
"sichuan.waiting.time",
216+
"sichuan.waiting.alloc",
217+
"sichuan.evaluate.time",
218+
"sichuan.evaluate.alloc",
219+
"sichuan.can-win.time",
220+
"sichuan.can-win.alloc"
221+
],
222+
"minimum_passes": 3,
223+
"must_pass": [
224+
"sichuan.waiting.time",
225+
"sichuan.evaluate.time",
226+
"sichuan.can-win.time"
227+
],
228+
"required_classes": [
229+
"top/ellan/mahjong/table/core/round/SichuanHuEvaluator.class"
230+
]
231+
},
232+
"region-apply": {
233+
"include": "^top\\.ellan\\.mahjong\\.table\\.render\\.RegionRenderApplyBenchmark\\.(?:planAndDeferRepresentativeTableRegions|applyStableStartedTable|precomputeCompleteStartedTableRegions)$",
234+
"benchmark_ids": [
235+
"region.apply-deferred.time",
236+
"region.apply-deferred.alloc",
237+
"region.apply-stable.time",
238+
"region.apply-stable.alloc",
239+
"region.complete-fingerprints.time",
240+
"region.complete-fingerprints.alloc"
241+
],
242+
"minimum_passes": 3,
243+
"must_pass": [
244+
"region.apply-deferred.time",
245+
"region.apply-stable.time",
246+
"region.complete-fingerprints.time"
247+
],
248+
"required_classes": [
249+
"top/ellan/mahjong/table/render/TableRegionDisplayCoordinator.class",
250+
"top/ellan/mahjong/table/render/TableRegionFingerprintService.class"
251+
]
252+
},
253+
"layout-cache": {
254+
"include": "^top\\.ellan\\.mahjong\\.table\\.core\\.SessionRenderLayoutCacheBenchmark\\.(?:stableSessionCacheHit|representativeSeatComponentMiss|representativeWallComponentMiss)$",
255+
"benchmark_ids": [
256+
"layout-cache.hit.time",
257+
"layout-cache.hit.alloc",
258+
"layout-cache.seat-miss.time",
259+
"layout-cache.seat-miss.alloc",
260+
"layout-cache.wall-miss.time",
261+
"layout-cache.wall-miss.alloc"
262+
],
263+
"minimum_passes": 3,
264+
"must_pass": [
265+
"layout-cache.hit.time",
266+
"layout-cache.seat-miss.time",
267+
"layout-cache.wall-miss.time"
268+
],
269+
"required_classes": [
270+
"top/ellan/mahjong/render/layout/TableRenderLayout.class",
271+
"top/ellan/mahjong/table/core/MahjongTableSession.class"
272+
]
273+
},
192274
"ray-proxy": {
193275
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
194276
"benchmark_ids": [
@@ -414,6 +496,150 @@
414496
"metric": "gc.alloc.rate.norm",
415497
"direction": "lower"
416498
},
499+
{
500+
"id": "riichi.impossible.time",
501+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.structurallyImpossibleCall",
502+
"metric": "primary",
503+
"direction": "lower"
504+
},
505+
{
506+
"id": "riichi.impossible.alloc",
507+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.structurallyImpossibleCall",
508+
"metric": "gc.alloc.rate.norm",
509+
"direction": "lower"
510+
},
511+
{
512+
"id": "riichi.ron-only.time",
513+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.structurallyImpossibleRonOnly",
514+
"metric": "primary",
515+
"direction": "lower"
516+
},
517+
{
518+
"id": "riichi.ron-only.alloc",
519+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.structurallyImpossibleRonOnly",
520+
"metric": "gc.alloc.rate.norm",
521+
"direction": "lower"
522+
},
523+
{
524+
"id": "riichi.pon.time",
525+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.firstPossiblePonAnalysis",
526+
"metric": "primary",
527+
"direction": "lower"
528+
},
529+
{
530+
"id": "riichi.pon.alloc",
531+
"benchmark": "top.ellan.mahjong.riichi.RiichiReactionBenchmark.firstPossiblePonAnalysis",
532+
"metric": "gc.alloc.rate.norm",
533+
"direction": "lower"
534+
},
535+
{
536+
"id": "sichuan.waiting.time",
537+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.waitingTilesStandardHand",
538+
"metric": "primary",
539+
"direction": "lower"
540+
},
541+
{
542+
"id": "sichuan.waiting.alloc",
543+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.waitingTilesStandardHand",
544+
"metric": "gc.alloc.rate.norm",
545+
"direction": "lower"
546+
},
547+
{
548+
"id": "sichuan.evaluate.time",
549+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.evaluateStandardWin",
550+
"metric": "primary",
551+
"direction": "lower"
552+
},
553+
{
554+
"id": "sichuan.evaluate.alloc",
555+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.evaluateStandardWin",
556+
"metric": "gc.alloc.rate.norm",
557+
"direction": "lower"
558+
},
559+
{
560+
"id": "sichuan.can-win.time",
561+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.canWinWithFixedMeld",
562+
"metric": "primary",
563+
"direction": "lower"
564+
},
565+
{
566+
"id": "sichuan.can-win.alloc",
567+
"benchmark": "top.ellan.mahjong.table.core.round.SichuanHuEvaluatorBenchmark.canWinWithFixedMeld",
568+
"metric": "gc.alloc.rate.norm",
569+
"direction": "lower"
570+
},
571+
{
572+
"id": "region.apply-deferred.time",
573+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.planAndDeferRepresentativeTableRegions",
574+
"metric": "primary",
575+
"direction": "lower"
576+
},
577+
{
578+
"id": "region.apply-deferred.alloc",
579+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.planAndDeferRepresentativeTableRegions",
580+
"metric": "gc.alloc.rate.norm",
581+
"direction": "lower"
582+
},
583+
{
584+
"id": "region.apply-stable.time",
585+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.applyStableStartedTable",
586+
"metric": "primary",
587+
"direction": "lower"
588+
},
589+
{
590+
"id": "region.apply-stable.alloc",
591+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.applyStableStartedTable",
592+
"metric": "gc.alloc.rate.norm",
593+
"direction": "lower"
594+
},
595+
{
596+
"id": "region.complete-fingerprints.time",
597+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.precomputeCompleteStartedTableRegions",
598+
"metric": "primary",
599+
"direction": "lower"
600+
},
601+
{
602+
"id": "region.complete-fingerprints.alloc",
603+
"benchmark": "top.ellan.mahjong.table.render.RegionRenderApplyBenchmark.precomputeCompleteStartedTableRegions",
604+
"metric": "gc.alloc.rate.norm",
605+
"direction": "lower"
606+
},
607+
{
608+
"id": "layout-cache.hit.time",
609+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.stableSessionCacheHit",
610+
"metric": "primary",
611+
"direction": "lower"
612+
},
613+
{
614+
"id": "layout-cache.hit.alloc",
615+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.stableSessionCacheHit",
616+
"metric": "gc.alloc.rate.norm",
617+
"direction": "lower"
618+
},
619+
{
620+
"id": "layout-cache.seat-miss.time",
621+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.representativeSeatComponentMiss",
622+
"metric": "primary",
623+
"direction": "lower"
624+
},
625+
{
626+
"id": "layout-cache.seat-miss.alloc",
627+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.representativeSeatComponentMiss",
628+
"metric": "gc.alloc.rate.norm",
629+
"direction": "lower"
630+
},
631+
{
632+
"id": "layout-cache.wall-miss.time",
633+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.representativeWallComponentMiss",
634+
"metric": "primary",
635+
"direction": "lower"
636+
},
637+
{
638+
"id": "layout-cache.wall-miss.alloc",
639+
"benchmark": "top.ellan.mahjong.table.core.SessionRenderLayoutCacheBenchmark.representativeWallComponentMiss",
640+
"metric": "gc.alloc.rate.norm",
641+
"direction": "lower"
642+
},
417643
{
418644
"id": "ray.viewers1.time",
419645
"benchmark": "top.ellan.mahjong.perf.RayProxyCoordinatorBenchmark.proxyPlan1Viewer",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package top.ellan.mahjong.riichi;
2+
3+
import java.lang.reflect.Field;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.UUID;
7+
import java.util.concurrent.TimeUnit;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Level;
11+
import org.openjdk.jmh.annotations.Mode;
12+
import org.openjdk.jmh.annotations.OutputTimeUnit;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import top.ellan.mahjong.riichi.model.MahjongTile;
17+
import top.ellan.mahjong.riichi.model.TileInstance;
18+
19+
@BenchmarkMode(Mode.AverageTime)
20+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
21+
public class RiichiReactionBenchmark {
22+
@Benchmark
23+
public ReactionOptions structurallyImpossibleCall(NoCallState state) {
24+
return state.player.reactionOptionsFor(state.discard, false, false);
25+
}
26+
27+
@Benchmark
28+
public ReactionOptions structurallyImpossibleRonOnly(NoCallState state) {
29+
return state.player.reactionOptionsFor(state.discard, false, true);
30+
}
31+
32+
@Benchmark
33+
public ReactionOptions firstPossiblePonAnalysis(PossibleCallState state) {
34+
return state.player.reactionOptionsFor(state.discard, false, false);
35+
}
36+
37+
@State(Scope.Thread)
38+
public static class NoCallState {
39+
private RiichiPlayerState player;
40+
private TileInstance discard;
41+
private Map<?, ?> furoReactions;
42+
43+
@Setup(Level.Trial)
44+
public void setUp() throws ReflectiveOperationException {
45+
this.player = player(List.of(
46+
MahjongTile.M1, MahjongTile.M4, MahjongTile.M7,
47+
MahjongTile.P1, MahjongTile.P4, MahjongTile.P7,
48+
MahjongTile.S1, MahjongTile.S4, MahjongTile.S7,
49+
MahjongTile.SOUTH, MahjongTile.WEST, MahjongTile.WHITE_DRAGON, MahjongTile.GREEN_DRAGON
50+
));
51+
this.discard = tile(100, MahjongTile.EAST);
52+
if (this.player.reactionOptionsFor(this.discard, false, false) != null) {
53+
throw new IllegalStateException("No-call reaction fixture changed");
54+
}
55+
Field cacheField = RiichiPlayerAnalysisState.class.getDeclaredField("cachedFuroReactions");
56+
cacheField.setAccessible(true);
57+
this.furoReactions = (Map<?, ?>) cacheField.get(this.player);
58+
}
59+
60+
@Setup(Level.Invocation)
61+
public void clearFuroReaction() {
62+
this.furoReactions.clear();
63+
}
64+
}
65+
66+
@State(Scope.Thread)
67+
public static class PossibleCallState {
68+
private RiichiPlayerState player;
69+
private TileInstance discard;
70+
71+
@Setup(Level.Invocation)
72+
public void setUp() {
73+
this.player = player(List.of(
74+
MahjongTile.M5_RED, MahjongTile.M5, MahjongTile.M1, MahjongTile.M9,
75+
MahjongTile.P1, MahjongTile.P9, MahjongTile.S1, MahjongTile.S9,
76+
MahjongTile.EAST, MahjongTile.SOUTH, MahjongTile.WEST,
77+
MahjongTile.WHITE_DRAGON, MahjongTile.GREEN_DRAGON
78+
));
79+
this.discard = tile(200, MahjongTile.M5);
80+
}
81+
}
82+
83+
private static RiichiPlayerState player(List<MahjongTile> hand) {
84+
RiichiPlayerState player = new RiichiPlayerState("benchmark", "benchmark", false);
85+
for (int index = 0; index < hand.size(); index++) {
86+
player.getHands().add(tile(index, hand.get(index)));
87+
}
88+
return player;
89+
}
90+
91+
private static TileInstance tile(int id, MahjongTile tile) {
92+
return new TileInstance(new UUID(0L, id + 1L), tile);
93+
}
94+
}

0 commit comments

Comments
 (0)