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 @@ -25,6 +25,7 @@ jobs:
{ name: "performance-ab", color: "d4c5f9", description: "Run the base-owned paired performance gate" },
{ 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-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 @@ -33,6 +33,7 @@ on:
options:
- snapshot
- gb-bot
- region-fingerprint
- ray-proxy
- infra
forks:
Expand Down Expand Up @@ -157,6 +158,7 @@ jobs:
choices = {
"performance-snapshot": "snapshot",
"performance-gb-bot": "gb-bot",
"performance-region-fingerprint": "region-fingerprint",
"performance-ray-proxy": "ray-proxy",
}
selected = [profile for label, profile in choices.items() if label in labels]
Expand Down
11 changes: 11 additions & 0 deletions docs/performance-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,20 @@ and exactly one profile label to a pure performance PR:

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

The region-fingerprint harness checks the complete region map and representative per-tile
results against an independent copy of the current `Objects.toString(value, "")`, colon-delimited,
per-character FNV contract before every JMH iteration. That behavior sentinel is outside the
timed section, so a candidate cannot gain by changing fingerprint encoding while the measured
time and normalized allocation remain focused on production code. The infrastructure
fingerprint profile applies the same rule to its exact delimited string and now treats
normalized allocation as a secondary guardrail.

The workflow is loaded through `pull_request_target`, uses only `contents: read`, persists no
checkout credentials, disables Gradle's shared cache, and clears GitHub/Actions runtime
credentials from every shell step that executes candidate bytecode. Candidate JMH runs use a
Expand Down
49 changes: 48 additions & 1 deletion perf/ab/gate-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"infra": {
"include": "^top\\.ellan\\.mahjong\\.perf\\.DelimitedFingerprintBuilderBenchmark\\.buildRepresentativeFingerprint$",
"benchmark_ids": [
"infra.fingerprint.time"
"infra.fingerprint.time",
"infra.fingerprint.alloc"
],
"minimum_passes": 1,
"must_pass": [
Expand Down Expand Up @@ -93,6 +94,22 @@
"top/ellan/mahjong/table/core/round/GbBotDecisionService.class"
]
},
"region-fingerprint": {
"include": "^top\\.ellan\\.mahjong\\.table\\.render\\.TableRegionFingerprintBenchmark\\.(?:precomputeStartedTableRegions|fingerprintRenderedTileBatch)$",
"benchmark_ids": [
"region.precompute.time",
"region.precompute.alloc",
"region.tiles.time",
"region.tiles.alloc"
],
"minimum_passes": 2,
"must_pass": [
"region.precompute.time"
],
"required_classes": [
"top/ellan/mahjong/table/render/TableRegionFingerprintService.class"
]
},
"ray-proxy": {
"include": "^top\\.ellan\\.mahjong\\.perf\\.RayProxyCoordinatorBenchmark\\.proxyPlan(?:1Viewer|4Viewers|32Viewers)$",
"benchmark_ids": [
Expand Down Expand Up @@ -120,6 +137,12 @@
"metric": "primary",
"direction": "lower"
},
{
"id": "infra.fingerprint.alloc",
"benchmark": "top.ellan.mahjong.perf.DelimitedFingerprintBuilderBenchmark.buildRepresentativeFingerprint",
"metric": "gc.alloc.rate.norm",
"direction": "lower"
},
{
"id": "snapshot.viewers4.time",
"benchmark": "top.ellan.mahjong.perf.RenderSnapshotViewerBenchmark.snapshotWith4Viewers",
Expand Down Expand Up @@ -192,6 +215,30 @@
"metric": "gc.alloc.rate.norm",
"direction": "lower"
},
{
"id": "region.precompute.time",
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.precomputeStartedTableRegions",
"metric": "primary",
"direction": "lower"
},
{
"id": "region.precompute.alloc",
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.precomputeStartedTableRegions",
"metric": "gc.alloc.rate.norm",
"direction": "lower"
},
{
"id": "region.tiles.time",
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.fingerprintRenderedTileBatch",
"metric": "primary",
"direction": "lower"
},
{
"id": "region.tiles.alloc",
"benchmark": "top.ellan.mahjong.table.render.TableRegionFingerprintBenchmark.fingerprintRenderedTileBatch",
"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
Expand Up @@ -3,11 +3,12 @@
import java.util.concurrent.TimeUnit;
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 org.openjdk.jmh.infra.Blackhole;
import top.ellan.mahjong.table.core.DelimitedFingerprintBuilder;

/**
Expand All @@ -21,9 +22,26 @@
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class DelimitedFingerprintBuilderBenchmark {
private static final String EXPECTED =
"seat-label:EAST:00000000-0000-0000-0000-000000000001:25000:true:false;wall:70:1";

@Setup(Level.Iteration)
public void verifyFingerprintContract() {
String value = buildFingerprint();
if (!EXPECTED.equals(value)) {
throw new IllegalStateException(
"Delimited fingerprint contract changed: expected=" + EXPECTED + ", actual=" + value
);
}
}

@Benchmark
public void buildRepresentativeFingerprint(Blackhole blackhole) {
String value = DelimitedFingerprintBuilder.create(192)
public String buildRepresentativeFingerprint() {
return buildFingerprint();
}

private static String buildFingerprint() {
return DelimitedFingerprintBuilder.create(192)
.field("seat-label")
.field("EAST")
.field("00000000-0000-0000-0000-000000000001")
Expand All @@ -35,6 +53,5 @@ public void buildRepresentativeFingerprint(Blackhole blackhole) {
.field(70)
.field(1)
.toString();
blackhole.consume(value);
}
}
Loading