Skip to content

Commit 0769c26

Browse files
committed
perf: add region fingerprint A/B profile
1 parent 457086e commit 0769c26

6 files changed

Lines changed: 560 additions & 5 deletions

File tree

.github/workflows/ensure-labels.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
{ name: "performance-ab", color: "d4c5f9", description: "Run the base-owned paired performance gate" },
2626
{ name: "performance-snapshot", color: "bfdadc", description: "Use the viewer snapshot performance profile" },
2727
{ name: "performance-gb-bot", color: "bfdadc", description: "Use the GB bot decision performance profile" },
28+
{ name: "performance-region-fingerprint", color: "bfdadc", description: "Use the table region fingerprint performance profile" },
2829
{ name: "performance-ray-proxy", color: "bfdadc", description: "Use the client ray-proxy performance profile" }
2930
];
3031

.github/workflows/performance-ab.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ on:
3333
options:
3434
- snapshot
3535
- gb-bot
36+
- region-fingerprint
3637
- ray-proxy
3738
- infra
3839
forks:
@@ -157,6 +158,7 @@ jobs:
157158
choices = {
158159
"performance-snapshot": "snapshot",
159160
"performance-gb-bot": "gb-bot",
161+
"performance-region-fingerprint": "region-fingerprint",
160162
"performance-ray-proxy": "ray-proxy",
161163
}
162164
selected = [profile for label, profile in choices.items() if label in labels]

docs/performance-testing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,20 @@ and exactly one profile label to a pure performance PR:
5252

5353
- `performance-snapshot` - real `TableRenderSnapshotFactory` fan-out at 4, 32 and 128 viewers;
5454
- `performance-gb-bot` - real `GbBotDecisionService` with duplicate, mixed and unique hands;
55+
- `performance-region-fingerprint` - a real started-table snapshot and layout, measuring the
56+
complete region map plus a batch of private/public hands, discards, melds and all 136 wall
57+
slots;
5558
- `performance-ray-proxy` - 1, 4 and 32-viewer coordinator lifecycle and unchanged-geometry
5659
reuse. This CPU/allocation profile does not measure protocol bytes or client hit coverage.
5760

61+
The region-fingerprint harness checks the complete region map and representative per-tile
62+
results against an independent copy of the current `Objects.toString(value, "")`, colon-delimited,
63+
per-character FNV contract before every JMH iteration. That behavior sentinel is outside the
64+
timed section, so a candidate cannot gain by changing fingerprint encoding while the measured
65+
time and normalized allocation remain focused on production code. The infrastructure
66+
fingerprint profile applies the same rule to its exact delimited string and now treats
67+
normalized allocation as a secondary guardrail.
68+
5869
The workflow is loaded through `pull_request_target`, uses only `contents: read`, persists no
5970
checkout credentials, disables Gradle's shared cache, and clears GitHub/Actions runtime
6071
credentials from every shell step that executes candidate bytecode. Candidate JMH runs use a

perf/ab/gate-config.json

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"infra": {
5050
"include": "^top\\.ellan\\.mahjong\\.perf\\.DelimitedFingerprintBuilderBenchmark\\.buildRepresentativeFingerprint$",
5151
"benchmark_ids": [
52-
"infra.fingerprint.time"
52+
"infra.fingerprint.time",
53+
"infra.fingerprint.alloc"
5354
],
5455
"minimum_passes": 1,
5556
"must_pass": [
@@ -93,6 +94,22 @@
9394
"top/ellan/mahjong/table/core/round/GbBotDecisionService.class"
9495
]
9596
},
97+
"region-fingerprint": {
98+
"include": "^top\\.ellan\\.mahjong\\.table\\.render\\.TableRegionFingerprintBenchmark\\.(?:precomputeStartedTableRegions|fingerprintRenderedTileBatch)$",
99+
"benchmark_ids": [
100+
"region.precompute.time",
101+
"region.precompute.alloc",
102+
"region.tiles.time",
103+
"region.tiles.alloc"
104+
],
105+
"minimum_passes": 2,
106+
"must_pass": [
107+
"region.precompute.time"
108+
],
109+
"required_classes": [
110+
"top/ellan/mahjong/table/render/TableRegionFingerprintService.class"
111+
]
112+
},
96113
"ray-proxy": {
97114
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
98115
"benchmark_ids": [
@@ -120,6 +137,12 @@
120137
"metric": "primary",
121138
"direction": "lower"
122139
},
140+
{
141+
"id": "infra.fingerprint.alloc",
142+
"benchmark": "top.ellan.mahjong.perf.DelimitedFingerprintBuilderBenchmark.buildRepresentativeFingerprint",
143+
"metric": "gc.alloc.rate.norm",
144+
"direction": "lower"
145+
},
123146
{
124147
"id": "snapshot.viewers4.time",
125148
"benchmark": "top.ellan.mahjong.perf.RenderSnapshotViewerBenchmark.snapshotWith4Viewers",
@@ -192,6 +215,30 @@
192215
"metric": "gc.alloc.rate.norm",
193216
"direction": "lower"
194217
},
218+
{
219+
"id": "region.precompute.time",
220+
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.precomputeStartedTableRegions",
221+
"metric": "primary",
222+
"direction": "lower"
223+
},
224+
{
225+
"id": "region.precompute.alloc",
226+
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.precomputeStartedTableRegions",
227+
"metric": "gc.alloc.rate.norm",
228+
"direction": "lower"
229+
},
230+
{
231+
"id": "region.tiles.time",
232+
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.fingerprintRenderedTileBatch",
233+
"metric": "primary",
234+
"direction": "lower"
235+
},
236+
{
237+
"id": "region.tiles.alloc",
238+
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.fingerprintRenderedTileBatch",
239+
"metric": "gc.alloc.rate.norm",
240+
"direction": "lower"
241+
},
195242
{
196243
"id": "ray.viewers1.time",
197244
"benchmark": "top.ellan.mahjong.perf.RayProxyCoordinatorBenchmark.proxyPlan1Viewer",

src/perfTest/java/top/ellan/mahjong/perf/DelimitedFingerprintBuilderBenchmark.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import java.util.concurrent.TimeUnit;
44
import org.openjdk.jmh.annotations.Benchmark;
55
import org.openjdk.jmh.annotations.BenchmarkMode;
6+
import org.openjdk.jmh.annotations.Level;
67
import org.openjdk.jmh.annotations.Mode;
78
import org.openjdk.jmh.annotations.OutputTimeUnit;
89
import org.openjdk.jmh.annotations.Scope;
10+
import org.openjdk.jmh.annotations.Setup;
911
import org.openjdk.jmh.annotations.State;
10-
import org.openjdk.jmh.infra.Blackhole;
1112
import top.ellan.mahjong.table.core.DelimitedFingerprintBuilder;
1213

1314
/**
@@ -21,9 +22,26 @@
2122
@BenchmarkMode(Mode.AverageTime)
2223
@OutputTimeUnit(TimeUnit.NANOSECONDS)
2324
public class DelimitedFingerprintBuilderBenchmark {
25+
private static final String EXPECTED =
26+
"seat-label:EAST:00000000-0000-0000-0000-000000000001:25000:true:false;wall:70:1";
27+
28+
@Setup(Level.Iteration)
29+
public void verifyFingerprintContract() {
30+
String value = buildFingerprint();
31+
if (!EXPECTED.equals(value)) {
32+
throw new IllegalStateException(
33+
"Delimited fingerprint contract changed: expected=" + EXPECTED + ", actual=" + value
34+
);
35+
}
36+
}
37+
2438
@Benchmark
25-
public void buildRepresentativeFingerprint(Blackhole blackhole) {
26-
String value = DelimitedFingerprintBuilder.create(192)
39+
public String buildRepresentativeFingerprint() {
40+
return buildFingerprint();
41+
}
42+
43+
private static String buildFingerprint() {
44+
return DelimitedFingerprintBuilder.create(192)
2745
.field("seat-label")
2846
.field("EAST")
2947
.field("00000000-0000-0000-0000-000000000001")
@@ -35,6 +53,5 @@ public void buildRepresentativeFingerprint(Blackhole blackhole) {
3553
.field(70)
3654
.field(1)
3755
.toString();
38-
blackhole.consume(value);
3956
}
4057
}

0 commit comments

Comments
 (0)