Skip to content

perf: layout/content fingerprint split + bot display-name cache (PR-D) - #129

Merged
Arbousier1 merged 6 commits into
devfrom
pr-d-layout-reconcile
Aug 5, 2026
Merged

perf: layout/content fingerprint split + bot display-name cache (PR-D)#129
Arbousier1 merged 6 commits into
devfrom
pr-d-layout-reconcile

Conversation

@Arbousier1

Copy link
Copy Markdown
Collaborator

Summary

Spark profile (https://spark.lucko.me/8yCLAsdfdV) shows applyRenderPrecompute at 27% of the main thread. Root cause: tile content fingerprints mixed in raw tile positions, so every hand re-arrangement (deal/draw/discard → hand size changes → all points shift) changed every tile fingerprint and forced full remove+respawn of 26+ regions per seat, while reconcile (teleport) was sampled near zero.

  1. Layout/content fingerprint split (TableRegionFingerprintService + TableRegionDisplayCoordinator):
    • hand-private / hand-public tile content fingerprints no longer include point; new privateHandLayoutFingerprint / publicHandLayoutFingerprint track positions separately (per-seat, once per apply).
    • Coordinator tracks per-region appliedLayoutFingerprints; short-circuit now requires content fingerprint AND layout fingerprint to match. Layout-only changes enter the render path and are satisfied by reconcile (entity teleport / smooth movement) instead of region respawn; respawn stays as the safe fallback.
    • No-op for non-hand regions (layoutFingerprint=0 ⇒ no layout gate).
  2. Bot display-name cache (MahjongTableSession): rendered i18n bot names cached per player+locale, invalidated on participant changes (removeBot/replaceBotWithPlayer/removePlayer/clearBotNamesForLifecycle). displayName was ~2000 samples of the profile (per-tick i18n rendering via LocalizedMessages).

Test plan

  • ./gradlew compileJava green locally
  • CI Test/Build (full test suite) green — includes existing DisplayEntities reconcile tests (identity/power/idempotency contracts)
  • A/B gate PASS_OPTIMIZED

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Arbousier1 Arbousier1 added performance-ab Run the base-owned paired performance gate performance-region-apply Use the complete region render apply performance profile and removed performance-ab Run the base-owned paired performance gate labels Aug 5, 2026
…spawn on re-arrangement

Spark profile (spark.lucko.me/8yCLAsdfdV) showed applyRenderPrecompute as the
largest mahjong hotspot (27%): every hand re-arrangement (deal/draw/discard)
shifted all tile positions, which changed every tile fingerprint (point was
part of the content fingerprint) and forced a full remove+respawn of 26+
regions per seat while reconcile (teleport) was almost never reached.

- handPrivateTileFingerprint/handPublicTileFingerprint no longer mix tile
  positions into the content fingerprint; new privateHandLayoutFingerprint /
  publicHandLayoutFingerprint track the layout separately.
- TableRegionDisplayCoordinator keeps per-region appliedLayoutFingerprints;
  a layout-only change now enters the render path and is satisfied by
  reconcile (entity teleport) instead of respawn, with respawn retained as a
  safe fallback.
- cache rendered bot display names (i18n) per player+locale, invalidated on
  participant changes, cutting per-tick displayName rendering.
@Arbousier1
Arbousier1 force-pushed the pr-d-layout-reconcile branch from f7e8875 to 20aeb8d Compare August 5, 2026 03:56
@Arbousier1 Arbousier1 added the performance-ab Run the base-owned paired performance gate label Aug 5, 2026
…mplete

The first A/B run showed a 39.8% regression on region.apply-deferred
because the per-seat layout fingerprints enumerated every tile point
(320-capacity builder, ~150 FNV mixes each) on every apply, even on the
short-circuit path.

Tile coordinates are fully determined by (seat, hand size, tile index,
selected indices) — all already present in the per-tile content
fingerprints once hand size is added to the public tile fingerprint —
so the layout fingerprint only needs to carry the hand structure:

- handPublicTileFingerprint now includes hand size, making public hand
  content fingerprints layout-complete again;
- privateHandLayoutFingerprint/publicHandLayoutFingerprint shrink to a
  few structural FNV fields (computed once per seat per apply, never
  enumerating coordinates);
- both short-circuit paths drop the per-region layout lookup and go
  back to a pure content-fingerprint comparison;
- the reconcile-vs-respawn decision on the update path keeps the
  layout gate: structure unchanged -> reconcile (teleport in place),
  structure changed (hand grew/shrunk) -> respawn.

Net effect: the every-tick apply fixed cost returns to baseline while
the real-game win (layout-only changes no longer poison every tile
fingerprint) is preserved.
The A/B rerun (dedicated runner, 8 iterations) shrank the apply-deferred
regression from -39.8% to -6.1% and flipped complete-fingerprints to
+3.04% PASS_OPTIMIZED, but the remaining regression tracks the extra
hand-size field added to handPublicTileFingerprint (one extra FNV mix
per tile per apply).

Tile index and hand size are packed into a single injective field
(tileIndex * 64 + handSize, both < 64 in every legal game state), so the
public tile fingerprint keeps exactly the pre-change field count and the
every-tick short-circuit fixed cost returns to baseline while the
layout-complete content semantics are preserved.
The A/B rerun still showed a -7.25% apply-deferred regression. The
benchmark drives the planning coordinator with a zero budget, so every
apply only pays the enqueue cost: fingerprint computation plus the
per-seat layout fingerprint precomputation, which is never consumed on
the short-circuit path.

Since tile coordinates are fully determined by (seat, hand size, tile
index, selected indices) — all covered by the content fingerprints —
the layout structure fingerprint reduces to the hand size itself. Use
the already-computed handSize value as the fingerprint (long) instead
of calling the fingerprint service, removing the last per-apply
computation added by the layout/content split. The reconcile-vs-respawn
gate keeps comparing the applied hand size with the current one.

The now-unused privateHandLayoutFingerprint/publicHandLayoutFingerprint
methods are removed.
…fingerprint

Shaves the last per-apply arithmetic (a multiply) off the short-circuit
path; field count and injectivity are unchanged.
Region fingerprints are compared per region key (hand-private:WIND:index),
so the type prefix field is redundant and costs one FNV mix per tile per
apply. Removing it from the hand tile fingerprints restores the
short-circuit fixed cost to below baseline on the apply benchmarks.
@Arbousier1
Arbousier1 merged commit 223b9ab into dev Aug 5, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance-ab Run the base-owned paired performance gate performance-region-apply Use the complete region render apply performance profile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant