Skip to content

fix: PDC ownership tags + orphan entity audit/cleanup for display leak - #130

Open
Arbousier1 wants to merge 17 commits into
mainfrom
pr-d-entity-audit
Open

fix: PDC ownership tags + orphan entity audit/cleanup for display leak#130
Arbousier1 wants to merge 17 commits into
mainfrom
pr-d-entity-audit

Conversation

@Arbousier1

Copy link
Copy Markdown
Collaborator

Summary

  • PDC 归属标记DisplayEntities 新增 5 个 PDC tag(table_id/session_id/role/slot/generation),TableRegionDisplayCoordinator 两处 respawn 路径(hand-private / hand-public)对每个新生成的实体写归属,generation 为协调器内单调递增代际计数
  • 孤儿扫描:新增 ManagedEntityAudit,只统计/清理确认无归属(无 table_id 或归属 table 不存在)的受管实体;CleanupSubcommand 提供 /mahjong cleanup 清理命令,DebugSubcommand 新增 entities 分支列出全部受管实体(位置/类型/归属/世界)
  • 启动扫描:插件 onEnable 尾部 runGlobal 延迟任务执行一次孤儿扫描(仅报告,不自动删)
  • 安全边界:绝不全局删除 Interaction/ItemDisplay,只清理 PDC 标记且归属失效的实体

Why

线上卡顿根因:JVM 堆顶满 → 频繁老年代 GC(5.33GB 存活对象)。heap dump 诊断前需要实体级归属信息,现有 mahjong:managed 标记无法区分归属/代际,无法判断泄漏实体属于哪张桌、哪一轮生成。

Test plan

  • ./gradlew compileJava compileTestJava -PmahjongJavaToolchain=25 -PmahjongJavaTarget=21
  • ./gradlew spotlessJavaCheck
  • 服务器:/mahjong debug entities 列出实体与归属;/mahjong cleanup 清理孤儿
  • 重启后观察启动扫描日志(orphan count 应为 0 或仅剩残留)

Adds a Map-based placeholder overload to LocalizedMessages (behaviourally
identical delegation to the TagResolver variant) so hot rendering paths can
be benchmarked through a stable placeholder representation, plus a new JMH
benchmark covering the Spark-sampled call sites (bot names, seat status,
empty seat, centre line) and an i18n gate profile with time/alloc metrics.
test_gate.py assumes benchmarks[0] is the infra entry; inserting the
i18n entries at the head broke validate_profile_config for the infra
profile (must_pass not subset of primary ids from the stubbed specs).
Keep array order stable by appending new entries at the tail.
std::regex construction compiles the pattern on every call; the
StringToHandtiles parse runs on each JNI evaluateFan/evaluateTing,
and std::__detail::_Compiler frames dominated native sampling. A
function-local static (C++11 thread-safe init) compiles once.
perf: add i18n benchmark profile for parametrised message rendering
perf(native): compile handtiles legality regex once as static
Spark sampling (j9YE2XtMP3) shows per-tick viewer rendering spends its
time in MiniMessage deserialization under LocalizedMessages.plain with
placeholder TagResolvers:

- Cache Map-based render/plain by (locale, key, sorted placeholder
  entries) via Caffeine (max 512): hot paths publicSeatStatus,
  publicCenterText and botDisplayName now hit a single deserialize per
  stable placeholder set instead of per tick.
- Add formatNumber + Map overloads to MessageService facade so hot
  callers can use the cached path without constructing TagResolvers.
- Reuse one PlayerActionSnapshotFactory per session (was 3-4 news per
  bot tick) and replace hasAction stream().anyMatch with a plain loop.

Behaviour is unchanged: placeholder maps are resolved by tag name, so
iteration order does not affect output; bundles are immutable after load.
BotActionSchedulerTest and SichuanBotPreparationSchedulerTest mock
MahjongTableSession, which now exposes actionSnapshotFactory(). Stub it
to return a real PlayerActionSnapshotFactory so the shared per-session
factory path is exercised by the scheduler tests.
perf(i18n): cache parametrised renders and reuse action snapshot factory
…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.
…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.
perf: layout/content fingerprint split + bot display-name cache (PR-D)
Tag every spawned region entity with table/session/role/slot/generation
in its persistent data container, add /mahjong cleanup (orphan-only)
and debug entities listing, and scan for orphans at plugin startup so
leaked displays can be attributed to a table and removed without ever
touching unrelated Interaction/ItemDisplay entities.
@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant