diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e2a8f2b4..876da9d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable maintenance updates to this fork are documented here. ## Unreleased +- Replace isolated point evaluation with equal-weight, multi-point focus in the current supported local KataGo search tree; adopt and save ordinary analysis, retain attention outlines after targets complete, and preserve focus across identical ReadBoard frames (#414). +- Use KataGo root visits for ordinary analysis totals and budgets, preserve same-stream evaluation updates and cache depth protection, and retain exact root counts, candidate order, and edge allocation in SGF (#414). +- Allow fresh point evaluation after returning to an accepted ReadBoard position without another helper frame; validate position semantics and engine synchronization, and reject malformed or stale frame publication (#444). - Keep thread-source controls visible and locked to CFG for empty or unrecognized local targets, without explanatory warnings (#437). - Keep remote connection labels with their fields after deferred Swing layout, align thread-source controls with the adjacent benchmark action, and show only current thread policy status (#437). - Keep thread-source labels aligned across local and remote entries, use concise benchmark button labels, and clear stale tooltips when switching targets (#437). diff --git a/docs/SNAPSHOT_NODE_KIND.md b/docs/SNAPSHOT_NODE_KIND.md index 36d30a3c3..1e5f8eaba 100644 --- a/docs/SNAPSHOT_NODE_KIND.md +++ b/docs/SNAPSHOT_NODE_KIND.md @@ -167,6 +167,10 @@ ReadBoard 协议里的 `pass` 行在自动落子/交换顺序链路中表示用 - `undo`、`play`(含真实 PASS)与 `set_position` 使用精确 numbered response identity 和既有 queued/pending retirement。错误、发送失败或超时使所属位置 lineage 失败;迟到响应不能恢复失败来源,也不能结算后继命令。完整替换位置建立新的 lineage,但本身仍需确认。 - `requireResponseBeforeSend` 的普通队列等待真实已写出的响应义务,不使用包含尚未发送后继命令的计数阻塞自身。Engine-game 的精确 ACK/terminal 与 arbitration 语义独立保留。 - 解析和最终节点 publication 均复验冻结目标。被退休、目标过期或未确认的位置输出不能进入候选缓存或 ownership-only backfill;有效历史缓存、SGF 已保存分析、同上下文暂停/继续与主副引擎独立槽位保留。显式失效仍由既有 `isChanged/isChanged2` 允许首个合法低 visits 接管。 +- KataGo 普通分析区分精确 root visits、候选 move visits、可选 edgeVisits 和推荐 order。总量与全局预算使用存在的 root;缺少 root 时保留 legacy 非对称候选求和。候选文字使用 move visits,分配比例使用存在的 edge(包括 0),排名沿用 order;节点评价仍来自首选候选。 +- 旧 legacy 缓存准入只用 incoming 的同一 legacy 算法比较;通过保护门后可以整份保存 incoming 的真实 root。已存 root 与缺失 root 的新流不可比较,未显式失效时保留缓存。同一已采纳输出 owner 的同 root 新帧可更新评价/PV;新请求、binding 或真实搜索重建不继承该例外。ownership-only 回填不改变候选、计算量口径或已采纳来源。 +- SGF 的 LZ/LZOP 及副槽在六个传统 header slots 后保存 `rootVisits=<精确整数>`,候选 PV 前保存 order 和存在的 edgeVisits。无标识数据保持 legacy/unknown;clone/restore 保留分析值但不复制活跃流刷新权限。扩展放在同一分析 payload 内,旧 writer 重建 payload 时一并降级。 +- 用户显式通过准入的同树 focus 可为当前节点对应引擎槽位授予整份普通分析采纳权限,允许首份合法精确 root payload 替换更高旧缓存。首份前取消/暂停/失败/context 变化退休未消费权限;已采纳值在完成、取消和同局面暂停后保留。仅 focus 参数重发延续同树刷新关系;真正清树、binding/位置替换仍退休旧权限。具体关注、进度与渲染行为见 `docs/TRACKING_ANALYSIS_CONTRACT.md`。 - 跨 SNAPSHOT 的复合恢复及 ReadBoard 单次最终 resume 遵守下列对应 owner 契约。 ## 复合局面恢复确认(Issue #429 / Ticket 02) diff --git a/docs/TRACKING_ANALYSIS_CONTRACT.md b/docs/TRACKING_ANALYSIS_CONTRACT.md index ae70921db..360cce908 100644 --- a/docs/TRACKING_ANALYSIS_CONTRACT.md +++ b/docs/TRACKING_ANALYSIS_CONTRACT.md @@ -1,168 +1,99 @@ # Tracking Analysis Contract -面向维护者的架构、取舍、踩坑与扩展说明见 -[`TRACKING_ANALYSIS_DEVELOPER_GUIDE.md`](TRACKING_ANALYSIS_DEVELOPER_GUIDE.md)。 - -“评估此点”复用当前前台本地 KataGo 的唯一 GTP stream。Production 不再创建第二个 -tracking KataGo 进程,也不保留 legacy runtime、console、preload、warning、keep-tracking、 -feature flag 或失败 fallback。 - -本合同补充 `docs/SNAPSHOT_NODE_KIND.md`;tracking 只显示 transient overlay,不改变其中的 -`MOVE`、`PASS`、`SNAPSHOT` 或 ReadBoard history 语义。 - -## Production 入口 - -- `LizzieFrame` 在一个 frame 生命周期内只实例化一个 `TrackingAnalysisController`。 -- 本地右键入口把 add/remove/clear 意图提交给该 controller。 -- 当前 `ReadBoard` 存在时,同一右键入口必须先通过 - `ReadBoardTrackingEligibilityAdapter` 复验 stable accepted frame;不稳定 frame 不发送 - tracking request,也不重试。 -- 两个入口只复用 `Lizzie.leelaz`。引擎必须 started、loaded、local direct、KataGo,且不在 - engine-game mode;Remote/WebSocket/SSH/double-engine 不在本合同范围。 -- Add 失败不显示 tracking-specific popup、`X` 或 retry 文案。 - -## Controller ownership - -`TrackingAnalysisController` 唯一拥有: - -- selected points、newest-first pending points 和每点 current attempt; -- immutable context、request generation 和 progress timeout; -- current stream-only lease handle; -- immutable `DisplaySnapshot`。 - -UI 只能提交 add/remove/clear。Controller 不启动、切换或重启引擎,不写 Board/history,也不写 -普通 `bestMoves`;仅本节明确允许的 clean handback 可凭首个 receipt 请求 `Leelaz` 恢复原 -ponder。 - -每个点使用一个独立 stream-only lease。Initial numbered stop 完整结束后只发送: - -```text -kata-analyze allow B 1 allow W 1 -``` - -只有目标坐标 visits 严格增加才续期 8 秒 progress timeout。达到该点 visits 后发送 final -numbered stop;只有 final fence 成功关闭后结果才标记 completed,然后调度 newest pending -point。Clean natural completion,以及用户显式 remove current / clear 后成功关闭的 final -fence,可按首个 receipt 恢复原 ponder;任何 handoff、ordinary release、context failure 或 -transport failure 都不得恢复。 - -## Context 与 ReadBoard - -Tracking context 至少绑定: - -- history identity 与 current display/history node identity; -- board size、完整 stones fingerprint、to-play、rules、komi; -- engine identity/incarnation; -- interval 与每点 visits; -- 可选 ReadBoard helper identity、revision、accepted node 与 board revision。 - -第一个点建立 context;后续 add 必须匹配。局面、history/display node、board size、stones、 -to-play、rules、komi、engine/incarnation、参数或 ReadBoard identity/revision/node 任一变化都 -立即清 selected/pending/result 并 release 当前 lease。 - -ReadBoard stable 条件与失效顺序由 `ReadBoardTrackingEligibilityAdapter` 和 `ReadBoard` -eligibility snapshot 拥有。Tracking 不拦截 helper protocol,不推导或补造 `MOVE/PASS`。 - -## Display 与 renderer - -- `DisplaySnapshot` 是 immutable value;renderer 不读取 controller 内部 mutable state。 -- Renderer 只在 snapshot 的 history identity 与 current history 相同,且 display node identity - 同时等于 current display/history node时绘制。 -- selected point 立即显示虚线环;remove pending 立即隐藏且不影响 current;remove current 或 - clear 立即隐藏全部相应 overlay,后台只完成安全归还。 -- selected point 立即占用该坐标并隐藏同坐标普通候选,默认显示圆角分段虚线外框;尚无结果时 - 使用中性灰。首份 visits 大于零的 current result 到达后立即显示 result,不等待 final - fence。用户关闭外框后,首份结果前不显示占位标记。 -- Tracking result 以同一局面当前普通最佳候选为动态基准,按 `MoveRankDefinition` 的胜率/目差 - 损失等级实时绘制虚线外框;普通分析更新时实时重算颜色。没有普通最佳候选时外框使用中性灰。 -- Tracking result 内部使用用户配置的固定颜色和不透明度,不随质量等级变化,默认颜色为 - `RGB(255, 156, 156)`,默认不透明度为 100%。外框默认开启,颜色由质量等级决定,不透明度 - 可配置。 -- Tracking result 文字复用普通候选的显示项、行序、字体缩放和定位;前景色按实际背景在黑/白 - 中选择较高对比度,也可关闭自动适配并使用自定义颜色。启用 score diff 时显示 tracking score - 减当前普通最佳候选 score。 -- Completed result 可在同一 context 内与新点并存。Tracking result 只画 overlay,不进入 - SGF、普通候选、胜率图或 history node。 -- Strict safe-GTP release 可冻结最后一个仍 valid result snapshot;其他 ordinary、typed - handoff、lifecycle、context 或 transport release 清空 selected/display。 - -## Stream arbitration - -`Leelaz` 唯一拥有 stream arbitration、initial/final fence、ordinary queue、typed handoff 和 -transport terminal/rebind settlement。锁序保持: - -```text -engineArbitrationLock -> commandQueue -``` - -- Ordinary command 先进入原 queue,再 claim tracking release;final fence 后由原 writer 按 - FIFO 写出。 -- Active tracking 中的普通落子若在同一次调用排队后续 ponder,只能在 `play` 成功响应后结算 - tracking stream 不提供的单格 ordinary response watermark;`play` 错误响应不得结算,也不得 - 提前接受新局面的 `info`。 -- Clean ponder handback 只能在 final fence 后排队原 ponder,并在该 streaming analysis 已排队时 - 结算同样的单格 watermark。 -- Safe raw GTP whitelist 仅包含无 caller ID、exact arity 的 `name`、`version`、 - `protocol_version`、`list_commands`、`known_command ` 和 `showboard`。 -- Safe query 发布 `SAFE_READ_ONLY_QUERY` 并冻结最后 valid overlay;其他 admitted ordinary - 发布 `ORDINARY_OPERATION` 并清空,二者都不自动 reacquire。 -- Typed foreground/retained-mode first winner 在 claim 时立即清 display,final fence 后激活 - existing target owner。第二 typed/lifecycle contender沿用 existing busy。 -- Destructive lifecycle 继续使用 existing reservation并立即执行原 action;不得保存 callback、 - continuation 或 user intent。 -- Final fence、terminal 或 transport 输出不确定时 fail-closed,ordinary queue 不能穿过可能 - 仍开放的 analysis stream。 - -## 配置兼容 - -Tracking 每点 visits 使用: - -```text -tracking-analysis-max-visits -``` - -该值可在“综合设置 → 引擎与分析”的“选点评估计算量”中调整。 - -选点评估结果外观使用以下 UI 配置: - -```text -show-tracking-point-outline -tracking-point-interior-color -tracking-point-interior-opacity -tracking-point-outline-opacity -tracking-point-text-auto-color -tracking-point-text-color -``` - -这些外观项位于“主题外观”设置页。两个 `opacity` 百分比均表示不透明度:0 为完全透明,100 为 -完全不透明。 - -加载旧配置时,若新 key 不存在,则迁移 `tracking-engine-max-visits` 的正值;随后删除旧 -`tracking-engine-max-visits`、`tracking-engine-preload` 和 -`tracking-engine-skip-warning`。旧 preload/engine command 不能启动第二进程。 - -## 测试与发布边界 - -- Production-entry tests 必须同时覆盖 local 与 stable ReadBoard route,并证明二者共享同一 - controller/current engine。 -- Controller、lease、ordinary、handoff、lifecycle、ReadBoard GMA、renderer stale-node gate - 和配置迁移必须有 deterministic headless coverage。 -- Windows integration harness 只存在于 test source,通过 public controller/lease/handoff - seam 使用 monotonic clock 输出原始 CSV/JSON;它必须捕获并恢复 default uncaught handler - 与 EDT EventQueue,Executor task 必须显式取得 `Future` 或 join。 -- 未设置真实引擎时,`TrackingWindowsIntegrationHarnessTest` 运行 controlled transport - characterization。Ticket 07 通过 `tracking.real.command` system property 或 - `TRACKING_REAL_COMMAND` environment variable 提供已批准的真实 KataGo 命令;harness 先做 - 3 次 warm-up,再采集 30 个 acquisition/handoff/target-operation 原始样本,输出 - `real-samples.csv`、`real-samples.json` 及 P50/P95/max。可用 - `tracking.real.output` 指定输出目录。 -- Windows GUI、真实 KataGo/OpenCL 与实际进程数仍需 Ticket 07 验收;本 source candidate 在 - 该 gate 前不得 merge 或 release。 - -## Out of scope - -- Remote Compute/WebSocket、Java SSH、外部 ssh/plink、double-engine tracking。 -- 第二 tracking process、dual-runtime route、feature flag 或 fallback。 -- Cross-point persistent lease、跨 context 恢复、结果持久化、PV overlay、SGF tracking 字段。 -- Ordinary response ledger、第二 queue、generic transaction/callback registry、rollback 或 - 自动重试。 +“评估此点”在当前前台、本地直连且已确认支持 move focus 的 KataGo 普通搜索树中追加研究。 +开发入口和维护说明见 [`TRACKING_ANALYSIS_DEVELOPER_GUIDE.md`](TRACKING_ANALYSIS_DEVELOPER_GUIDE.md)。 +本合同补充 [`SNAPSHOT_NODE_KIND.md`](SNAPSHOT_NODE_KIND.md),不改变 MOVE、PASS、SNAPSHOT、 +setup、dummy PASS、exact restore 或 ReadBoard history 的更高权威语义。 + +## 准入与能力 + +- `LizzieFrame.addTrackingPoint` 是本地和稳定 ReadBoard 的共同入口,同一 frame 只有一个 + `TrackingAnalysisController`,只使用当前 `Lizzie.leelaz`。 +- Remote、SSH、WebSocket、双引擎、对局、GMA、Web trial、foreground 独占任务和未恢复位置 + 不准入用户 focus;这些模式保留自己的既有 owner 与错误处理。 +- move focus 是 analyze 参数,`list_commands` 或版本字符串不构成支持证明。 +- 每个实际 engine incarnation 最多一次受控能力探针。在位置已确认、合法 idle owner 且普通 + ponder 尚未启动时发送合法 pass、probability 0 和 rootInfo 的 analyze,再结束分析。 + 启动成功和 stop/final 完整响应边界均消费后才能标为 SUPPORTED。 +- 语法拒绝标为 UNSUPPORTED,普通分析仍可用。通信、发送和 fence 不确定按当前 binding 的 + 既有失败路径处理,不把通信故障缓存成“不支持”。旧探针不改变替换 binding 的能力或分析意图。 +- 探针不打断活跃普通流;正常分析交接完成后才开放用户准入,不能由迟到的启动覆盖新 focus。 +- 用户暂停时不启动探针;探针结束后的普通分析恢复与主线分析控制共用暂停锁,同时检查暂停 + 代次和界面暂停状态。成功、语法拒绝及 lifecycle 延迟回调均不得绕过用户暂停。 + +## 关注、进度与取消 + +`TrackingAnalysisController` 唯一拥有用户关注点、其中尚需增益的活动点、每点累计目标、context、 +进度高水位、8 秒无进展时钟及 immutable `DisplaySnapshot`。Controller 不拥有评价副本、引擎 +进程、writer、queue、lease、receipt 或 handoff。 + +- 多点等权,总 focus probability 固定为 0.5;目标沿用每点累计 N visits。 +- 进度读取裁剪前的当前合法普通 payload。旧 SGF 或其他树的候选不能证明当前树达标。 +- 当前 live 已达目标可立即采纳,不重发 focus。报告中同时达标的点合并成一次集合更新。 +- 达标只移出活动集合、停止该点时钟,关注保留;后续普通输出不自动重新添加增益。 +- 只有该点 visits 严格超过已观察高水位才续期 8 秒;root 或其他点增长不续期。 + 占用、受限、非法或无进展的点不能被标为完成。 +- remove/clear 立即移除相应关注与外圈,安全撤去尚在运行的增益;不回滚已采纳普通分析。 +- 达标、请求取消和引擎已确认撤去增益是不同状态。取消在途或失败时,不显示为已确认撤去。 +- 暂停、时限、root 总量限制、超时、错误和 context 退休不自动重试、不恢复旧增益。 +- 落子、导航离开、换谱、引擎切换/重启及语义失效清关注,返回或恢复不复活旧意图。 + +## 普通命令与搜索树 + +`Leelaz` 唯一拥有 ordinary queue/writer、numbered response、fence、timeout、reader binding、 +output ownership/generation 和位置 lineage。锁序维持 `engineArbitrationLock -> commandQueue`, +controller 通知不得在引擎 ownership locks 内执行。 + +- 调整 focus 只重发普通 analyze,保持位置、轮次、allow/avoid、输出选项及非 focus 参数。 + 不 clear、loadsgf、重放或修改允许集合;普通开始时间和总量预算不因重发重置。 +- 先结束旧 GTP response,再将新输出绑定新请求;请求编号变化本身不表示清树。 +- 相同集合、相同 ReadBoard 完整帧不重发。原队列内取消和 `QueuedCommand.beginOutputWrite` + 竞争同一物理写入边界,未写出的旧更新不能复活已移除点。 +- 未确认旧 stream 已结束或通信已失效时,后续普通写入不能穿过该边界。 +- 共享 foreground、GMA、retained mode 与 lifecycle owner 保留;focus 不再持有独占 tracking lease。 + +## Live、节点缓存与 SGF + +- parser 和最终 publication 均复验 reader/incarnation、输出归属、位置 lineage、Board identity/ + revision、display node 和引擎槽位。focus payload 必须有精确 root,不能以候选 visits 和替代。 +- 普通非 focus 分析维持既有缓存保护。合法显式 focus 为当前节点、槽位、树建立采纳权限; + 首份合法完整 payload 整槽接管,允许新 root1000 替换旧 root10000,不拼接两棵树的候选。 +- 拒绝、发送失败、首份前 remove/clear/暂停/context 变化均保留旧缓存并退休未消费权限。 + 仍有效的完整 live snapshot 可在请求接受时直接采纳。 +- 首次接管后,同树同 root 的新评价继续刷新。只有 focus 重发延续同树关系;真正清树、 + binding/位置替换不能继承旧采纳权限,不能根据 visits 猜测来源。 +- 主副槽独立。新树接管清旧树 ownership;同树后续缺失地图保留仍有效的同树地图。 +- 完成、取消和同局面暂停不回滚已采纳结果;导航和引擎退休保留节点值、退休旧输出权限。 +- 显示和 SGF 保存使用同一份普通节点分析,沿用 rootVisits/order/edgeVisits 的既有 payload + 格式。SGF 不保存或恢复关注点、目标、活动集合、timer 或已完成/取消的 focus 意图。 + +## ReadBoard + +- accepted remote evidence、当前新请求准入和已有请求有效性分别判断。 +- FRAME_PENDING/纯 SYNCING 关闭新请求准入,但保留属于最后 accepted 语义的 focus;相同 + 完整帧不清关注、不重启分析。坏帧、停止、helper 退休和真实语义变化仍退休旧请求。 +- 导航离开清关注但保留可重验的远端证据;返回时完整重验盘面、history/node、轮次、尺寸、 + rules/komi、当前 Board revision 和已确认的 engine incarnation,无新 helper frame 也可新建。 +- processing epoch 隔离迟到接受;LF/CRLF 保持,非法单元整帧拒绝。Adapter 的失效 listener + 走 `contextChanged(null)`,不是用户 clear。 + +## 显示与配置 + +- 候选本体、文字、填充/透明度、字体、显示项和 order 完全走普通绘制;不存在第二份结果圆。 +- 关注质量外圈使用同一普通 payload 的该候选与 order 0 候选,按 `MoveRankDefinition` + 胜率/目差损失实时配色;缺候选/基准时中性显示。质量外圈不表示仍在追加计算。 +- 达标和同局面暂停保留质量外圈,活动进度结束;取消关注立即清圈。普通蓝点只服从引擎 order。 +- 仍关注的点豁免数量/低比例过滤,包含已达标和外圈关闭时;取消关注恢复普通过滤,不删分析。 +- `show-tracking-point-outline` 只控制外圈;重开不发 focus、不恢复已清关注。 +- 保留 `tracking-analysis-max-visits` 和 `tracking-point-outline-opacity`。旧正值 visits 配置 + 按既有规则迁移;独立结果填充和文字外观配置删除,不再读取或保存。 +- 全部语言资源同次保持 keys、顺序、placeholder parity,说明评估会更新当前局面普通分析。 + +## 验证与发布边界 + +T1 经过 production add/remove/clear、真实 Leelaz queue/writer/response/parser/publication; +T2 经过真实 ReadBoard 帧和导航;T3 使用普通 payload、renderer 和生产 SGF parse/adopt/save。 +受控 transport 只替代外部进程,不能伪造准入、缓存或 owner 校验。 + +真实 KataGo 保树证据、实际窗口绘制和自动化回归分别记录。固定实验候选证明不解除正式 +KataGo 能力发布门禁 G3;#445 final-fence 和完整 Windows 整合门禁 G4 仍归后续验收票。 diff --git a/docs/TRACKING_ANALYSIS_DEVELOPER_GUIDE.md b/docs/TRACKING_ANALYSIS_DEVELOPER_GUIDE.md index 3fd72a015..8d05deb23 100644 --- a/docs/TRACKING_ANALYSIS_DEVELOPER_GUIDE.md +++ b/docs/TRACKING_ANALYSIS_DEVELOPER_GUIDE.md @@ -1,507 +1,89 @@ -# 选点评估(Tracking Analysis)开发指南 +# 同树选点评估开发指南 -本文面向继续维护“评估此点”功能的开发者,说明当前 single-stream 实现为什么这样设计、 -各模块分别拥有哪部分状态、曾经踩过哪些坑,以及修改时需要守住哪些边界。 +行为以 [Tracking Analysis Contract](TRACKING_ANALYSIS_CONTRACT.md) 为准;历史与同步语义继续服从 +[Snapshot Node Kind](SNAPSHOT_NODE_KIND.md)。内部类名和仍有用途的配置键保留 `tracking`, +其含义是用户关注,不再表示独立评价进程或独占评估流。 -行为要求以 [Tracking Analysis Contract](TRACKING_ANALYSIS_CONTRACT.md) 为准;涉及棋局历史、 -ReadBoard、`MOVE`、`PASS` 或 `SNAPSHOT` 时,还必须先读 -[Snapshot Node Kind](SNAPSHOT_NODE_KIND.md)。本文负责解释实现和经验,不替代合同。 +## 模块边界 -## 1. 术语与命名 +- `LizzieFrame` 捕获当前 history/node、尺寸、盘面、轮次、规则、贴目、引擎 incarnation 和参数, + 将本地或 ReadBoard 入口交给同一个 controller。 +- `TrackingAnalysisController` 管理关注集合、活动集合、累计 visits 高水位和每点超时。 + `DisplaySnapshot` 是 immutable;其中没有胜率、目差、PV 或另一份候选结果。 +- `Leelaz` 独占普通命令队列、物理 writer、响应边界、输出归属和 transport 生命周期。 + focus 集合变化使用普通 analyze 重发,不调用 ponder 重置预算,不清树重放。 +- `BoardData` 处理整份普通分析的采纳与槽位保护;`BoardRenderer` 使用普通候选绘制本体, + 只额外叠加关注质量圈和独立进度指示。 +- `ReadBoard` 管理 accepted evidence 与当前请求准入;其 listener 退休 context,不执行用户 clear。 -- 用户界面统一称“选点评估”“评估此点”“评估结果”。 -- Java 类、资源键、配置键和历史文档仍使用 `tracking`。不要为了统一文案机械重命名内部 - 标识符,否则会扩大配置迁移、测试和兼容范围。 -- **选中点(selected point)**:用户当前希望评估的坐标,包括正在计算、等待计算和已经完成 - 的点。 -- **当前尝试(current attempt)**:当前唯一占用 KataGo analysis stream 的点。 -- **等待点(pending point)**:尚未取得 stream lease 的点;采用 newest-first 顺序。 -- **stream-only lease**:只独占一段 GTP 输出流,不修改或恢复棋盘的独占会话。 -- **initial/final fence**:由带编号的 `stop` 和完整响应边界建立的开始/结束隔离线。 -- **ordinary command**:沿 `Leelaz` 原有普通 FIFO queue 发送的命令。 -- **typed handoff**:把 tracking stream 交给已有的 foreground analysis 或 retained engine mode - owner;它不是通用 callback 或用户意图队列。 -- **clean handback**:final fence 成功后,在严格复验 receipt 的前提下恢复 tracking 前的普通 - ponder。 +## 准入与能力探针 -## 2. 为什么改成 single-stream +能力探针由初始化完成、位置已确认的生产入口调用 `startMoveFocusProbeAfterInitialization`。 +不要把探针挂到每次 `ponder()`:导航恢复和手动继续分析不是初始化,会被额外 stream 打断。 +探针使用 pass、probability 0 与 rootInfo;分析启动成功及 stop/final 空行完整消费后才支持用户 +focus。语法拒绝与通信不确定分别处理。每个实际 binding 只尝试一次,旧 completion 不能启动 +替换引擎或改变其能力。普通分析交接完成后才开放准入,避免迟到普通命令覆盖用户 focus。 -旧实现为选点评估启动第二个 KataGo 进程。它带来三个根本问题: +初始化探针和完成后的恢复必须尊重 `isUserAnalysisPaused()`,并与 +`pauseForAnalysisControl` 共用 `analysisControlPonderLock`。探针还检查自己的暂停代次; +lifecycle 释放后的 fallback 使用 `ponderIfAnalysisControlAllows()`,不能直接 `ponder()`。 +`TrackingProductionCutoverTest` 覆盖启动前暂停、成功/拒绝响应中暂停和已有界面暂停状态。 -1. 第二个进程增加 GPU/显存和进程管理成本。 -2. 它的棋盘、规则、贴目、ReadBoard 会话和前台引擎生命周期容易与真实前台状态脱节。 -3. 每个普通操作都可能需要额外的“停止第二引擎并重试”适配,边界持续扩张。 +## 请求到结果 -当前实现只复用当时的前台、本地直连、已加载且支持 `kata-analyze` 的 KataGo。目标不是让 -tracking 成为新的全局调度器,而是让它临时租用现有 stream,并由原 owner 在需要时安全取回。 +1. `addTrackingPoint` 捕获并复验当前 context;ReadBoard 请求额外通过完整 stable-frame 重验。 +2. Controller 校验坐标、占用和已有 allow/avoid 限制。合法显式请求向 Leelaz 申请当前树的 + scoped adoption;拒绝不改变缓存。 +3. 有仍有效的完整 live payload 时立即采纳,已达累计目标则无需发送增益。 +4. 更新活动集合时先结束旧 response,然后通过原 queue 写出同一 base analyze 加 focus 集合。 + 多点等权、总 probability 0.5;其他分析参数保持。 +5. 合法普通 payload 同时更新 live、节点分析和 controller 进度。每个点只有自身 visits 新高 + 才延长 8 秒时钟;同帧完成一次更新集合。 +6. 达标结束活动计算但保留关注;remove/clear 立即隐藏关注,取消确认仍等待真实响应边界。 -## 3. 设计目标与明确非目标 +## 缓存与持久化 -### 目标 +普通分析沿用保护高缓存的策略。显式合法 focus 才允许当前节点对应槽位整份接管较高旧缓存; +真实 root 下降如实显示和保存。首份前取消、拒绝、发送失败、暂停或 context 变化退休未消费 +权限。首份之后同树同 root 评价可继续更新;真正清树、导航、引擎替换不继承旧输出权限。 -- 一个 KataGo 进程、一条前台 GTP stream。 -- 用户一次点击即可继续落子、导航、分析、切换或重启,不保存第二次点击意图。 -- 选点评估只产生 transient overlay,不写 `Board`、history、SGF、普通候选或胜率图。 -- 每个点有独立 visits 预算;新加入的等待点优先。 -- final fence 或 transport 状态不确定时 fail-closed,绝不让普通 parser 接收来源不明的 - analysis 输出。 -- future owner 尽量通过已有 queue、foreground handoff 或 lifecycle reservation 自然继承 - tracking 交互。 +新树采用时旧 ownership 地图退役;同树缺新地图保留有效旧地图。不要只把目标点拼进旧候选, +不要用候选访问量推断搜索树来源。SGF 使用既有 `rootVisits`、`order`、`edgeVisits` 扩展, +不保存关注/活动集合、目标或 timer。 -### 非目标 +## 取消与锁序 -- Remote Compute、WebSocket、Java SSH、外部 ssh/plink 或 double-engine tracking。 -- 第二个 tracking 引擎、fallback runtime、dual-runtime feature flag。 -- 跨点长期持有一个 lease、跨 context 恢复、结果持久化、PV overlay 或 SGF tracking 字段。 -- 第二 ordinary queue、ordinary response ledger、ID stripping、通用 transaction、callback - registry、rollback 或保存/重放用户意图。 -- 由 controller 向前台引擎发送 `play`、`undo` 或 replay 来构造局面。 -- 在本功能中实现围棋 suicide/ko/superko 合法性判断。 +原 `QueuedCommand.beginOutputWrite()` 是实际写出许可;取消未写出的 focus 必须与它竞争, +不是只从 queue 列表移除。已取得许可的命令继续完成必要的 stop/fence,不能声称字节已撤回。 +故障或 fence 不确定时 fail-closed,普通命令不能穿过开放 stream。 -一旦需求必须引入上述能力,应先更新合同并重新设计,不能用局部 callback 或 fallback 偷渡。 +保持 `engineArbitrationLock -> commandQueue`,controller 通知在引擎 ownership locks 外。 +focus 不拥有独占 lease;foreground、GMA、retained mode 和 lifecycle 保持各自原 owner。 +重启实际绑定替换仍须建立现有 bootstrap receipt,不能随旧 tracking 专属分支一起删除。 -## 4. 组件与所有权 +## ReadBoard 与显示 -```text -RightClickMenu - | - v -LizzieFrame -----------------------> ReadBoardTrackingEligibilityAdapter - | | - v | before/after snapshot -TrackingAnalysisController <----------------------+ - | - | acquire/release stream-only lease - v -Leelaz - |- ExclusiveGtpSession / numbered fences - |- existing ordinary command queue - |- typed foreground/retained handoff - |- lifecycle and restart ownership - `- reader binding / transport terminal settlement +FRAME_PENDING/纯 SYNCING 只关闭新请求,相同完整 accepted 语义保留关注、进度和分析流。 +坏帧、停止、helper 退休、真实语义变化仍清请求。导航离开退休旧 context,返回可通过完整重验 +重新准入,不要求新 frame,也不复活旧关注。 -TrackingAnalysisController -- immutable DisplaySnapshot --> BoardRenderer -``` +候选本体必须和普通绘制完全相同,蓝点由 order 决定。质量圈比较同份合法普通 payload 的 +目标候选和 order 0 候选,复用 `MoveRankDefinition`;缺基准中性显示。关注集合不受外圈开关 +或完成状态影响,低 rank/edge0 仍可见;取消关注重走普通过滤而不删除分析数据。 -| 模块 | 唯一负责的状态 | 不应该负责 | -| --- | --- | --- | -| `TrackingAnalysisController` | selected/pending/current、context、generation、progress timeout、结果和 immutable snapshot | 启动/切换引擎,修改棋盘,发送普通命令 | -| `Leelaz` | stream arbitration、fence、queue、handoff、reader incarnation、terminal/rebind settlement | 决定 UI overlay 内容,保存业务参数 | -| `ReadBoard` | helper identity、stable eligibility、revision、accepted frame、GMA retirement | 直接控制 tracking lease,推导缺失的 `MOVE/PASS` | -| `LizzieFrame` | production entry、构造当前 context、选择 local 或 ReadBoard route | 持有 controller 内部可变集合 | -| `BoardRenderer` | 消费 immutable snapshot,并复验 display node | 读取 controller 内部状态或写回结果 | -| foreground/PLAY_MODE/GMA/lifecycle owner | 自己的参数、激活和失败清理 | 把 tracking 当作通用任务队列 | +仅保留累计 visits 目标、外圈开关与不透明度。全语言资源变化运行 parity,不保留独立结果 +内部颜色/透明度/文字配置或第二套结果渲染。 -关键原则是:每份可变状态只有一个 recovery authority。其他模块只能提交意图或报告结果, -不能再做一次补偿恢复。 +## 验证 -## 5. 一次选点评估的生命周期 +- `TrackingProductionCutoverTest`:production add/remove/clear、普通 queue/reader/publication、 + ReadBoard frame/navigation、缓存/SGF、暂停/缺 root/逐点超时及实际 renderer 路径。 +- `TrackingAnalysisControllerTest`:未知能力拒绝用户请求。 +- `TrackingConfigMigrationTest`、`LocalizationResourceParityTest`:配置 cutover 和资源一致性。 +- `PositionConfirmedRollbackTest` 与现有 foreground/GMA/lifecycle/SGF 套件守护共享 owner。 +- 固定真实 KataGo 的保树 smoke 与实际窗口绘制独立留证;受控 transport 不等于原生验收。 -### 5.1 添加点 +先运行覆盖变更的聚焦测试,最终共享边界整合再运行全套。不要以故障注入的 stderr 判失败, +以进程退出码和 Surefire failures/errors 为准。不要全仓格式化。 -1. `RightClickMenu` 把棋盘坐标交给 `LizzieFrame.addTrackingPoint(...)`。 -2. `LizzieFrame.currentTrackingContext()` 捕获 history/node、棋盘、行棋方、规则、贴目、引擎、 - reader incarnation、interval、visits,以及可选 ReadBoard context。 -3. 若 ReadBoard 存在,adapter 在 acquisition 前检查 stable snapshot,注册 identity invalidation - listener,并在 acquisition 后再次检查同一 frame。 -4. Controller 校验坐标与 context。已有 current attempt 时,新点进入 deque 头部;否则创建新的 - generation 并请求 lease。 -5. `Leelaz` 完成 initial fence 后调用 ready callback。只有 acquisition validator 仍成立才发送: - - ```text - kata-analyze allow B 1 allow W 1 - ``` - -6. Controller 只接受目标坐标且 visits 严格增加的 `info`。首份有效结果立即进入 snapshot; - 达到目标 visits 后请求 release。 -7. final fence 成功关闭后,结果才标记为 completed,然后处理最新的 pending point。 - -### 5.2 状态机 - -```text -IDLE - -> ACQUIRING initial fence 尚未完成 - -> ACTIVE tracking kata-analyze 已取得 stream - -> RELEASING 已请求 final fence - -> CLOSED final fence 或 terminal settlement 已完成 -``` - -`TrackingReleaseDisposition` 是与上述阶段正交的显示/归还语义,不要混为一个状态机: - -```text -ACTIVE -> FROZEN_BY_SAFE -> CLEARED -``` - -它只能单调升级:安全查询可冻结最后的有效结果;任何普通操作、typed handoff、lifecycle 或 -transport invalidation 都清空显示。后到的安全查询不能把 `CLEARED` 降回 frozen。 - -### 5.3 remove、clear 与 invalidate 的区别 - -- remove pending:立即从 selected/pending 移除,不触碰 current lease。 -- remove current:立即隐藏该点,标记 current cancelled,后台完成 final fence;clean close - 后允许恢复原 ponder。 -- 用户 clear:立即清空所有显示和 pending,current 只负责安全关闭;clean close 后允许恢复 - 原 ponder。 -- context/lifecycle/transport invalidate:清空 generation、receipt 和结果,旧 callback 变成 - stale no-op;不得恢复旧 ponder。 - -这一区分不能被一个“通用清理方法”抹平。特别是 `clear()` 与 -`contextChanged(null)` 的恢复语义不同。 - -### 5.4 clean handback - -Controller 保存第一份 acquisition receipt,而不是保存一个 `Runnable`。最后一个点关闭后, -只有同时满足以下条件才调用 `restorePonderAfterTracking(...)`: - -- natural completion,或用户显式 remove current / clear; -- final fence 无 failure; -- disposition 仍为 `ACTIVE`; -- receipt 的 engine identity 与 incarnation 仍匹配; -- 当前没有 lifecycle、ordinary writer、trial 或其他 owner 竞争。 - -失败只返回 false,不重试、不另建 owner、不恢复旧局面的 analysis。 - -## 6. Context 为什么这么“重” - -Tracking context 绑定以下事实: - -- history identity; -- current display/history node identity; -- board width/height 与完整 stones fingerprint; -- to-play、rules、komi; -- engine identity 与 reader incarnation; -- interval 与每点 target visits; -- 可选 ReadBoard helper identity、revision、node identity 与 board revision。 - -这些不是缓存优化,而是结果有效性的证明。只比较坐标、手数或 Zobrist 不足以区分 history -替换、规则/贴目变化、旧 reader 回调或 ReadBoard helper replacement。 - -Controller generation 同时隔离旧 line、旧 timer、旧 ready/closed callback。任何异步入口都应 -先用 captured generation 和 exact lease identity 复验,再接触 current state。 - -## 7. Stream arbitration 与普通命令 - -### 7.1 锁序 - -唯一允许的锁序是: - -```text -engineArbitrationLock -> commandQueue -``` - -callback、observer、target activation/failure 和 queue wakeup 应在 ownership locks 外执行。 -不要为了方便在 callback 中反向取得 arbitration lock。 - -### 7.2 为什么 ordinary command 必须先入原 queue - -普通操作的 first-winner 判定以既有 queue entry 为准: - -1. 在原 queue 建立 entry; -2. 再 claim tracking release; -3. final fence 后由原 writer 按 FIFO 发送。 - -`QueuedCommand.beginOutputWrite()` 才是实际 writer acceptance。若先 release tracking、后创建 -queue entry,typed handoff 或 lifecycle 可能插入空窗,用户的一次点击会丢失或错误变成 busy。 - -不要给 tracking 创建 command-name adapter。`komi`、`boardsize`、position estimate、manual -genmove 和未来 ordinary command 应继续走各自已有入口与同一 queue。 - -### 7.3 active tracking 中落子后的 ponder watermark - -落子期间可能同时排队: - -```text -play -kata-analyze ... # 新局面的普通 ponder -``` - -Tracking stream 不提供 ordinary parser 所需的单格 response watermark。只有 `play` 收到成功 -响应后,才能结算后续 ponder 的 watermark;写出成功不等于落子成功。`play` 错误时不得接受 -新局面的 `info`,也不得假装分析已经恢复。 - -### 7.4 safe raw GTP - -Active tracking 期间只有无 caller ID、exact arity 的以下命令属于安全只读查询: - -- `name` -- `version` -- `protocol_version` -- `list_commands` -- `known_command ` -- `showboard` - -额外参数、caller ID、未知命令或看似只读但未证明安全的 KataGo 命令都不能静默穿过边界。 -安全查询冻结已有结果但不自动 reacquire;普通操作把 disposition 升级为 `CLEARED`。 - -## 8. Typed handoff、lifecycle 与 Web trial - -### Typed handoff - -- 容量为 1,只支持已有的 `FOREGROUND_ANALYSIS` 与 `RETAINED_ENGINE_MODE`。 -- target 自己持有业务参数;`Leelaz` 只捕获 target identity/kind。 -- claim 时立即清 tracking display,但 target 不得提前改业务状态或发送 bytes。 -- final fence 后先转交 gate,再在 locks 外复验并激活一次。 -- terminal、rebind、context invalidation 或 target cancellation 只能 exactly-once fail,不能既 fail - 又 activate。 - -### Destructive lifecycle - -restart、switch、close、benchmark/contribution 沿用已有 lifecycle reservation。winner 立即执行 -原 destructive action,不等待 callback 保存的 continuation。销毁或 rebind transport 的原路径 -负责退休 tracking。 - -Restart bootstrap receipt 只是绑定 existing lifecycle owner 与 exact reader/output 的私有凭据, -不是第三种 owner。startup、parser settings 和 board restore 仍走原 ordinary queue。 - -### Web trial - -Web trial 与 tracking 严格互斥: - -- tracking acquiring/active/closing 时,`enter_trial` 返回现有 `engine_busy`,且零 trial mutation; -- trial active 或 exit resync 未 settle 时,tracking、foreground、用户 lifecycle、GMA 和 - PLAY_MODE 都沿用 existing busy; -- 不保存 enter intent,不自动重试,不把 trial 做成 typed tracking handoff。 - -## 9. ReadBoard 边界 - -ReadBoard route 要求 current helper 的 stable accepted frame,同时没有 first-frame、HOLD、sync、 -pending local move、GMA、unrestored engine 或 node mismatch。具体 reason 由 -`ReadBoardTrackingEligibilityAdapter.Snapshot` 表达。 - -Adapter 必须做 acquisition 前后双检,因为以下竞态真实存在: - -```text -读取 stable frame - -> 开始取得 tracking lease - -> helper/frame/node/revision 变化 - -> initial fence ready -``` - -第二次检查失败时不得发送 tracking request,也不得重试。EOF、`IOException`、shutdown 和 helper -replacement 还必须先退休旧 identity/generation,再清理 matching GMA;晚到 response 不能恢复 -已退休会话。 - -Tracking 不拦截 ReadBoard helper protocol,也不从 snapshot 猜测 `MOVE/PASS`。如果 GMA 缺少 -authoritative post-play frame,应修 ReadBoard producer,不能放宽 Lizzie 的 accepted-frame gate。 - -## 10. Display 与配置 - -Renderer 只读取 immutable `DisplaySnapshot`,并同时复验: - -- snapshot history identity 等于 current history; -- snapshot display node identity 等于 current history node; -- `LizzieFrame.getDisplayNode()` 仍是该 current node; -- 主棋盘且没有 branch preview。 - -选中点立即压过同坐标普通候选。无结果时外框为中性灰;有结果后以当前普通最佳候选为动态 -基准,用 `MoveRankDefinition` 的胜率/目差损失等级决定外框颜色。内部颜色固定、可配置,文字 -复用普通候选布局,并按实际合成背景选择黑/白前景。 - -当前配置键: - -```text -tracking-analysis-max-visits -show-tracking-point-outline -tracking-point-interior-color -tracking-point-interior-opacity -tracking-point-outline-opacity -tracking-point-text-auto-color -tracking-point-text-color -``` - -旧 `tracking-engine-max-visits` 只用于一次性迁移;旧 preload/engine command 不能再启动第二进程。 -资源键和配置键仍保留 `tracking`,不要因用户文案改成“选点评估”而破坏兼容。 - -## 11. 主要设计取舍 - -| 选择 | 收益 | 代价 | -| --- | --- | --- | -| 每个点一个 lease | ownership 与结果提交边界封闭 | 每点多一次 initial/final fence 往返 | -| stream-only 而非 foreground restore | 不 replay 棋盘,不污染 Board | 需要严格 context fingerprint | -| newest-first pending | 新关注的点更快得到结果 | 不是传统 FIFO | -| final fence 后才 completed | 不把仍可能输出的 stream 当成已关闭 | UI 完成状态晚于首份结果 | -| invalidation fail-closed | 不恢复错误局面或旧 reader | 某些失败不会自动继续分析 | -| strict ReadBoard stable frame | 不消费不完整/过期棋盘 | Stop Sync 等状态下入口可能不可用 | -| Web trial 严格互斥 | owner 模型有限、可验证 | 用户必须先结束另一模式再重试 | -| 修 caller 而非泛化 rebind | 修改面小,保留既有语义 | 每个真实 caller 必须证明 identity/reservation | - -曾讨论过跨点复用 lease 来减少延迟,但这会形成 cross-point persistent owner,并扩大 timeout、 -cancel、context change 与普通抢占矩阵。没有新的性能证据和合同批准前不要实现。 - -## 12. 已踩过的坑 - -### 12.1 ordinary response ledger 会无限扩大维护面 - -早期方案试图给普通命令分配 tracking-private ID,再做 ID stripping 和 response 分类。它要求理解 -所有旧 parser、streaming response、caller-supplied ID 和未来命令,因此被放弃。正确边界是复用 -existing exclusive session 的 numbered fence,而不是重写所有 ordinary response。 - -### 12.2 方法级 rebind 修复不等于 production caller 修复 - -曾尝试让 `initializeStreams(...)` 结算任意 foreground owner,迅速演变成 gate promotion、callback -reentry 和跨 owner transaction。实际可达问题是 benchmark restore 可能重启了“当前引擎”而非 -原 paused engine。最终修复 exact engine identity 和 caller reservation,而不是泛化底层 rebind。 - -### 12.3 first-winner 顺序写反会丢操作 - -普通操作必须先建立 queue entry;handoff 必须先建立 typed target gate;lifecycle 必须在 mutation -前取得 reservation。只检查“当前有没有 tracking session”不够,因为 closing session 与 pending -gate 之间也不能出现 admission 空窗。 - -### 12.4 callback 在锁内执行会造成死锁和丢 wakeup - -observer、target fail/activate、state-reset callback 即使抛异常,也不能阻止 gate cleanup 或原 -writer wakeup。先在锁内提交单一结果,再在锁外通知。 - -### 12.5 final fence 失败不能假装成功 - -如果 final stop 写失败、超时、reader EOF 或输出归属不确定,普通 queue 不能继续穿过。正确行为 -是 fault/terminal 当前 transport,并让 owner exactly-once 失败,而不是清标记后继续发送。 - -### 12.6 “写出了 play”不等于“可以接受新局面分析” - -只有成功 `play` response 才能结算后续 ponder watermark。否则错误局面的 `info` 可能被当作新 -局面分析,表现为落子成功/失败状态和分析按钮不一致。 - -### 12.7 clear 与 invalidate 不能混用 - -用户 remove/clear 允许 clean handback;context、参数、engine 和 lifecycle invalidation 不允许。 -调用方若只看到“都是清空显示”就复用同一个入口,会在错误局面恢复 ponder。 - -### 12.8 只做 acquisition 前 ReadBoard 检查存在 TOCTOU - -Stable frame 可能在 initial fence 完成前失效。必须同时比较 helper identity、revision、node 和 -board revision,并让旧 generation callback 失效。 - -### 12.9 generic loading 文案不能证明 tracking 失败 - -`WaitForAnalysis` 的“引擎加载中...”也可能来自 legacy flash 的 zero-request settlement 缺陷。 -排障时要分别查看 request count、lease/handoff availability、final fence 和 dispatch completion, -不能仅凭 UI 文案归因。 - -### 12.10 测试中的异常输出通常是故障注入 - -大量测试会主动打印 EOF、timeout、partial write、parser exception 或 process destroy failure。 -必须以 Maven exit code 和 `target/surefire-reports/TEST-*.xml` 的 failures/errors 为准。 - -### 12.11 WSL 与 Windows full suite 不能并行 - -WebBoard 固定端口会碰撞。两边必须串行运行。Windows temp cleanup 偶发失败需要 focused rerun, -再用一次 fresh serial full run 作为有效证据。 - -### 12.12 不要全仓格式化 - -仓库 `fmt:check` 有既存不合规基线,包含 `Leelaz.java` 等大文件。全仓 format 会制造与功能无关 -的大 diff。只保持 touched hunk 风格,并运行 `git diff --check`。 - -## 13. 当前已知合同漂移 - -当前 `ReadBoardTrackingEligibilityAdapter` 将 helper/frame invalidation listener 注册为 -`controller::clear`。但 `clear()` 在用户显式 clear 的 clean final fence 后允许恢复 ponder;合同 -要求 ReadBoard/context invalidation fail-closed,不恢复旧 ponder。 - -现有测试证明 invalidation 会清 display、阻止 tracking request,却没有锁定 ponder handback。 -在该语义得到明确修复或合同调整前: - -- 不要把这条 listener 写法复制到新的 context source; -- 新 invalidation route 应调用无 handback 的 context invalidation 语义; -- 若要修复,先增加 production-entry RED,分别证明用户 clear 恢复、ReadBoard invalidation 不恢复; -- 不要在文档更新中顺手改变运行行为。 - -## 14. 扩展功能时怎么选入口 - -### 新增普通 GTP 操作 - -沿原 `sendCommand`/queue 入口入队,再 claim tracking release。需要 UI 状态的操作,应在 -`beginOutputWrite()` 时 arm,并在 cancel/write failure/reset/rebind 中 exactly-once cleanup。 - -### 新增 foreground 或 retained owner - -优先复用现有 typed category。若现有两类无法表达,不要把任意 `Runnable` 塞入 handoff;先证明 -为什么需要新的 ownership category,并更新合同、first-winner 和 failure matrix。 - -### 新增 lifecycle 操作 - -继续使用 existing lifecycle reservation。reservation 成功后立即执行原 destructive action; -不要把 action 保存到 tracking closed callback。 - -### 新增 ReadBoard eligibility 条件 - -由 ReadBoard snapshot 增加明确 reason/revision,并在 adapter before/after acquisition 双检。 -不要让 controller 读取 ReadBoard mutable state。 - -### 新增 overlay 或显示字段 - -把它放进 immutable `DisplaySnapshot`/`PointResult`,renderer 仍必须通过 stale-node gate。不要写入 -普通 `bestMoves` 或 history node 来“借用”现有渲染。 - -### 修改文案或配置 - -文案可以本地化为“选点评估”,内部 key 默认保持兼容。配置变化必须使旧 context invalid,迁移 -需要 `TrackingConfigMigrationTest` 和 localization parity 覆盖。 - -## 15. 测试地图 - -| 测试 | 主要职责 | -| --- | --- | -| `TrackingAnalysisControllerTest` | add/remove/clear、顺序、context/generation、timeout、snapshot、handback、ReadBoard adapter | -| `LeelazTrackingStreamLeaseTest` | fence、queue、handoff、lifecycle、safe GTP、rebind、timeout、write failure、ordinary state settlement | -| `TrackingProductionCutoverTest` | right-click/ReadBoard 生产入口、单 controller/engine、恢复分析、renderer 和 UI 行为 | -| `WebTrialSingleStreamExclusionTest` | tracking/trial first-winner、enter/exit 空窗、零副作用 busy | -| `Ticket07RestartBootstrapProductionEntryTest` | restart receipt、startup/restore/fence 与生产入口 | -| `EngineManagerLifecycleReservationTest` | switch/restart/close 等 lifecycle first-winner | -| `LeelazReadBoardGmaTest`、`ReadBoardShutdownTest`、`ReadBoardSyncDecisionTest` | stable eligibility、GMA、retirement、late response | -| `TrackingWindowsIntegrationHarnessTest` | public seam 的 controlled/real KataGo 计时与异常捕获 | -| `TrackingConfigMigrationTest`、localization parity tests | 配置迁移、设置入口和资源键一致性 | - -推荐的 WSL/headless 命令: - -```bash -unset DISPLAY && mvn -q -Dfmt.skip=true -Djava.awt.headless=true \ - -Dtest=TrackingAnalysisControllerTest,LeelazTrackingStreamLeaseTest,TrackingProductionCutoverTest test - -unset DISPLAY && mvn -q clean test -Dfmt.skip=true -Djava.awt.headless=true -``` - -完整测试后应统计 Surefire XML,而不是只看安静模式输出。 - -## 16. 排障顺序 - -1. 记录 exact worktree、branch、HEAD、engine 类型和是否有 ReadBoard/Web trial。 -2. 判断问题属于 controller state、stream arbitration、business owner、renderer 还是 ReadBoard - producer,不要先按 UI 症状归因。 -3. 确认 current lease 阶段与 disposition;区分 ACQUIRING、ACTIVE、closing 和 frozen。 -4. 查看 ordinary queue entry 是否先建立、writer 是否达到 `beginOutputWrite()`。 -5. 核对 initial/final numbered fence、reader incarnation、output identity 和 terminal winner。 -6. 核对 context 的 history/node/engine/parameters/ReadBoard revision 是否变化。 -7. 用 latch/barrier/captured stream 建 deterministic RED,禁止用 sleep 猜时序。 -8. 修复真正 owner;若需要第二 queue、callback registry、retry intent 或新 permit,Stop/Replan。 -9. 跑 focused、tracking parity、fresh full WSL;桌面行为再到独立 Windows validation tree 串行验收。 -10. 明确区分自动化、Windows GUI、真实 TensorRT/OpenCL、Remote/SSH 等已运行与未运行证据。 - -## 17. Review checklist - -- [ ] 没有启动第二个 tracking KataGo。 -- [ ] 没有新增 ordinary queue、response ledger、ID stripping 或任意 callback registry。 -- [ ] 锁序仍为 `engineArbitrationLock -> commandQueue`,通知在锁外。 -- [ ] ordinary/typed/lifecycle 的 first-winner 在 mutation 和 bytes 前建立。 -- [ ] final fence 或 transport 不确定时 fail-closed。 -- [ ] stale generation、reader incarnation、engine/output identity 都会拒绝晚到事件。 -- [ ] 用户 clear 与内部 invalidation 的 ponder handback 语义没有混淆。 -- [ ] ReadBoard 在 acquisition 前后复验 stable frame,且不推导 `MOVE/PASS`。 -- [ ] renderer 只消费 immutable snapshot,并保留 stale-node/branch gate。 -- [ ] tracking result 不进入 Board/history/SGF/普通候选。 -- [ ] 配置和本地化 key 保持兼容,参数变化会 invalidate context。 -- [ ] 测试覆盖 first-winner、timeout、terminal、rebind、callback exception 和 exactly-once cleanup。 -- [ ] 验证报告明确列出 Windows/真实引擎/Remote 等未运行项。 - -## 18. 关键源码入口 - -- [`TrackingAnalysisController`](../src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java) -- [`Leelaz`](../src/main/java/featurecat/lizzie/analysis/Leelaz.java) -- [`ReadBoardTrackingEligibilityAdapter`](../src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java) -- [`ReadBoard`](../src/main/java/featurecat/lizzie/analysis/ReadBoard.java) -- [`LizzieFrame`](../src/main/java/featurecat/lizzie/gui/LizzieFrame.java) -- [`RightClickMenu`](../src/main/java/featurecat/lizzie/gui/RightClickMenu.java) -- [`BoardRenderer`](../src/main/java/featurecat/lizzie/gui/BoardRenderer.java) -- [`WebBoardManager`](../src/main/java/featurecat/lizzie/gui/web/WebBoardManager.java) +正式发布能力 G3 与 #445/Windows 整合 G4 仍由后续票验收;固定实验候选的通过不能替代它们。 diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index e57a17256..a14893e39 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -1038,11 +1038,7 @@ private static boolean analysisCommandUsesJavaSsh(JSONObject leelaz) { public int wholeGameAnalysisDeepVisits = WholeGameAnalysisOptions.DEFAULT_VISITS; public int trackingAnalysisMaxVisits = 500; public boolean showTrackingPointOutline = true; - public Color trackingPointInteriorColor = new Color(255, 156, 156); - public int trackingPointInteriorOpacityPercent = 100; public int trackingPointOutlineOpacityPercent = 92; - public boolean trackingPointTextAutoColor = true; - public Color trackingPointTextColor = Color.BLACK; public boolean autoQuickAnalyzeOnLoad = true; public boolean quickAnalysisLightweightModelEnabled = false; public String analysisSpecificRules = ""; @@ -2915,33 +2911,29 @@ static int migrateTrackingAnalysisConfig(JSONObject ui) { ui.remove("tracking-engine-preload"); ui.remove("tracking-engine-skip-warning"); ui.remove("tracking-engine-max-visits"); + removeRetiredTrackingAppearanceConfig(ui); return visits; } void loadTrackingPointAppearanceConfig(JSONObject ui) { showTrackingPointOutline = ui.optBoolean("show-tracking-point-outline", true); - trackingPointInteriorColor = - Theme.array2Color( - ui.optJSONArray("tracking-point-interior-color"), new Color(255, 156, 156)); - trackingPointInteriorOpacityPercent = - clampPercent(ui.optInt("tracking-point-interior-opacity", 100)); trackingPointOutlineOpacityPercent = clampPercent(ui.optInt("tracking-point-outline-opacity", 92)); - trackingPointTextAutoColor = ui.optBoolean("tracking-point-text-auto-color", true); - trackingPointTextColor = - Theme.array2Color(ui.optJSONArray("tracking-point-text-color"), Color.BLACK); + removeRetiredTrackingAppearanceConfig(ui); } public void saveTrackingPointAppearanceConfig() { uiConfig.put("show-tracking-point-outline", showTrackingPointOutline); - uiConfig.put( - "tracking-point-interior-color", Theme.color2ArrayNoAlpha(trackingPointInteriorColor)); - uiConfig.put( - "tracking-point-interior-opacity", clampPercent(trackingPointInteriorOpacityPercent)); uiConfig.put( "tracking-point-outline-opacity", clampPercent(trackingPointOutlineOpacityPercent)); - uiConfig.put("tracking-point-text-auto-color", trackingPointTextAutoColor); - uiConfig.put("tracking-point-text-color", Theme.color2ArrayNoAlpha(trackingPointTextColor)); + removeRetiredTrackingAppearanceConfig(uiConfig); + } + + private static void removeRetiredTrackingAppearanceConfig(JSONObject ui) { + ui.remove("tracking-point-interior-color"); + ui.remove("tracking-point-interior-opacity"); + ui.remove("tracking-point-text-auto-color"); + ui.remove("tracking-point-text-color"); } private static int clampPercent(int value) { diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index aa925fd9c..a7f8d882e 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -1249,8 +1249,10 @@ public static PreparedEngineReadyPublication prepareInitializeAfterVersionCheck( } if (!isEngineGame && currentFrame != null && !currentFrame.isPlayingAgainstLeelaz) { if (startPondering && !Lizzie.config.notStartPondering) { - engine.ponderIfAnalysisControlAllows(); - engine.setResponseUpToDate(); + if (!engine.startMoveFocusProbeAfterInitialization()) { + engine.ponderIfAnalysisControlAllows(); + engine.setResponseUpToDate(); + } } else { engine.notPondering(); engine.setResponseUpToDate(); diff --git a/src/main/java/featurecat/lizzie/analysis/AnalysisEngine.java b/src/main/java/featurecat/lizzie/analysis/AnalysisEngine.java index 070ff27aa..5bdcdd5e0 100644 --- a/src/main/java/featurecat/lizzie/analysis/AnalysisEngine.java +++ b/src/main/java/featurecat/lizzie/analysis/AnalysisEngine.java @@ -165,13 +165,9 @@ public interface ProgressListener { private boolean sharedForegroundLeaseStarting; private boolean sharedForegroundLeaseActive; private Leelaz.ForegroundAnalysisLease sharedForegroundLease; - private ForegroundRequestTarget sharedForegroundHandoffOwner; private boolean sharedForegroundRestoreInProgress; private Runnable sharedForegroundRestoreCompletion; private Runnable sharedForegroundRestoreFailure; - private ForegroundRequestTarget pendingForegroundRequest; - private boolean sharedForegroundRulesCapturePending; - private long foregroundRequestGeneration; private Leelaz.ExclusiveGtpLeaseAvailability foregroundLeaseAvailability; public AnalysisEngine(boolean isPreLoad) throws IOException { @@ -555,10 +551,14 @@ public void parseResult(String line) { } int reportedPlayouts = MoveData.getPlayouts(moves); JSONObject rootInfo = result.optJSONObject("rootInfo"); - if (rootInfo != null) { - reportedPlayouts = Math.max(reportedPlayouts, rootInfo.optInt("visits", 0)); + if (rootInfo != null && rootInfo.has("visits")) { + reportedPlayouts = rootInfo.getInt("visits"); } boolean payloadApplied = applyAnalysisPayload(node, moves, ownershipArray, reportedPlayouts); + if (payloadApplied && node.getData().bestMoves == moves + && rootInfo != null && rootInfo.has("visits")) { + node.getData().rootVisits = reportedPlayouts; + } responseCount++; if (payloadApplied) { resultCount++; @@ -636,9 +636,10 @@ private boolean handleRemoteGtpLine(String line) { if (job == null || !trimmed.startsWith("info ")) { return false; } - List moves = parseRemoteGtpInfo(trimmed); - RemoteGtpRootInfo rootInfo = parseRemoteGtpRootInfo(trimmed); - int playouts = rootInfo != null ? rootInfo.visits : MoveData.getPlayouts(moves); + KataGoAnalysisPayload payload = KataGoAnalysisPayload.parse(trimmed); + List moves = remoteGtpMoves(payload); + RemoteGtpRootInfo rootInfo = remoteGtpRootInfo(payload); + int playouts = payload.totalVisits(); if (moves.isEmpty() || playouts <= 0) { return true; } @@ -649,66 +650,21 @@ private boolean handleRemoteGtpLine(String line) { return true; } - private List parseRemoteGtpInfo(String line) { - String payload = line.startsWith("info ") ? line.substring("info ".length()) : line; - int rootInfoIndex = payload.indexOf(" rootInfo "); - if (rootInfoIndex >= 0) { - payload = payload.substring(0, rootInfoIndex); - } - List moves = new ArrayList(); - String[] variations = payload.split(" info "); - for (String variation : variations) { - if (variation == null || variation.trim().isEmpty()) { - continue; - } - try { - MoveData move = MoveData.fromInfoKatago(variation); - if ((move.variation == null || move.variation.isEmpty()) - && move.coordinate != null - && !move.coordinate.trim().isEmpty()) { - move.variation = new ArrayList(); - move.variation.add(move.coordinate); - } - moves.add(move); - } catch (RuntimeException ignored) { + private List remoteGtpMoves(KataGoAnalysisPayload payload) { + for (MoveData move : payload.moves) { + if ((move.variation == null || move.variation.isEmpty()) + && move.coordinate != null && !move.coordinate.isEmpty()) { + move.variation = new ArrayList<>(); + move.variation.add(move.coordinate); } } - return moves; + return payload.moves; } - private RemoteGtpRootInfo parseRemoteGtpRootInfo(String line) { - if (line == null) { - return null; - } - int rootInfoIndex = line.indexOf(" rootInfo "); - if (rootInfoIndex < 0) { - return null; - } - String[] tokens = line.substring(rootInfoIndex + " rootInfo ".length()).trim().split("\\s+"); - int visits = -1; - double winrate = Double.NaN; - double scoreLead = Double.NaN; - for (int i = 0; i < tokens.length - 1; i++) { - String key = tokens[i]; - String value = tokens[i + 1]; - try { - if ("visits".equals(key)) { - visits = Integer.parseInt(value); - i++; - } else if ("winrate".equals(key)) { - winrate = Double.parseDouble(value) * 100.0; - i++; - } else if ("scoreLead".equals(key) || "scoreMean".equals(key)) { - scoreLead = Double.parseDouble(value); - i++; - } - } catch (NumberFormatException ignored) { - } - } - if (visits <= 0 || !Double.isFinite(winrate) || !Double.isFinite(scoreLead)) { - return null; - } - return new RemoteGtpRootInfo(visits, Math.max(0.0, Math.min(100.0, winrate)), scoreLead); + private RemoteGtpRootInfo remoteGtpRootInfo(KataGoAnalysisPayload payload) { + if (payload.rootVisits < 0) return null; + return new RemoteGtpRootInfo( + payload.rootVisits, payload.rootWinrate, payload.rootScoreMean); } private void completeRemoteGtpJob( @@ -815,13 +771,7 @@ private boolean handleRemoteGtpRulesResponse(String line) { } Leelaz engine = sharedForegroundEngine; Leelaz.ForegroundAnalysisLease lease = sharedForegroundLease; - ForegroundRequestTarget handoffOwner = sharedForegroundHandoffOwner; - boolean restoreRulesCaptured = - engine != null - && (lease != null - ? lease.setRestoreRules(payload) - : handoffOwner != null - && engine.setForegroundAnalysisLeaseRestoreRules(handoffOwner, payload)); + boolean restoreRulesCaptured = engine != null && lease != null && lease.setRestoreRules(payload); if (!restoreRulesCaptured) { failRemoteGtpSetup(); return true; @@ -829,7 +779,6 @@ private boolean handleRemoteGtpRulesResponse(String line) { remoteGtpExpectedRulesCommandId = 0; cancelRemoteGtpSetupAckTimeout(); sharedForegroundOriginalRules = payload; - sharedForegroundRulesCapturePending = false; if (!startNextRemoteGtpJob()) { failRemoteGtpSetup(); } @@ -867,13 +816,11 @@ private void rememberRemoteGtpStoppingResult(String line) { if (remoteGtpStoppingJob == null) { return; } - List moves = parseRemoteGtpInfo(line); - if (!moves.isEmpty() && MoveData.getPlayouts(moves) > 0) { + KataGoAnalysisPayload payload = KataGoAnalysisPayload.parse(line); + List moves = remoteGtpMoves(payload); + if (!moves.isEmpty() && payload.totalVisits() > 0) { remoteGtpStoppingMoves = moves; - } - RemoteGtpRootInfo rootInfo = parseRemoteGtpRootInfo(line); - if (rootInfo != null) { - remoteGtpStoppingRootInfo = rootInfo; + remoteGtpStoppingRootInfo = remoteGtpRootInfo(payload); } } @@ -914,11 +861,12 @@ private boolean commitRemoteGtpStoppingResult() { } int reportedPlayouts = rootInfo != null ? rootInfo.visits : MoveData.getPlayouts(moves); boolean payloadApplied = applyAnalysisPayload(job.node, moves, null, reportedPlayouts); - if (payloadApplied && rootInfo != null) { + if (payloadApplied && job.node.getData().bestMoves == moves && rootInfo != null) { BoardData data = job.node.getData(); - data.winrate = rootInfo.winrate; - data.scoreMean = rootInfo.scoreLead; + if (Double.isFinite(rootInfo.winrate)) data.winrate = rootInfo.winrate; + if (Double.isFinite(rootInfo.scoreLead)) data.scoreMean = rootInfo.scoreLead; data.setPlayouts(rootInfo.visits); + data.rootVisits = rootInfo.visits; data.comment = SGFParser.formatComment(job.node); Lizzie.board.updateMovelist(job.node); } @@ -1190,10 +1138,6 @@ public void startRequestAllBranches() { public void startRequestAllBranches(boolean showProgressDialog) { if (!isLoaded) return; - ForegroundRequestTarget target = ForegroundRequestTarget.allBranches(this, showProgressDialog); - if (tryClaimForegroundTrackingHandoff(target)) { - return; - } startRequestAllBranchesNow(showProgressDialog); finishSuccessfulEmptyRequestIfNeeded(); } @@ -1222,11 +1166,6 @@ public void startRequest(int startMove, int endMove) { public void startRequest(int startMove, int endMove, boolean showProgressDialog) { if (!isLoaded) return; - ForegroundRequestTarget target = - ForegroundRequestTarget.mainline(this, startMove, endMove, showProgressDialog); - if (tryClaimForegroundTrackingHandoff(target)) { - return; - } startRequestNow(startMove, endMove, showProgressDialog); finishSuccessfulEmptyRequestIfNeeded(); } @@ -1255,79 +1194,6 @@ private void startRequestNow(int startMove, int endMove, boolean showProgressDia showRequestProgressOrContinueBatch(showProgressDialog); } - private boolean tryClaimForegroundTrackingHandoff(ForegroundRequestTarget target) { - Leelaz engine = sharedForegroundEngine; - if (engine == null) { - return false; - } - synchronized (this) { - if (shutdownRequested) { - return false; - } - if (pendingForegroundRequest != null) { - foregroundLeaseAvailability = Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE; - target.fail(Leelaz.TrackingHandoffFailure.ACTIVATION_FAILED); - return true; - } - target.generation = ++foregroundRequestGeneration; - pendingForegroundRequest = target; - } - Leelaz.TrackingHandoffClaim claim = engine.claimTrackingHandoff(target); - target.claim = claim; - if (claim.availability() == Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING) { - boolean stillCurrent; - synchronized (this) { - stillCurrent = pendingForegroundRequest == target && !shutdownRequested; - } - if (!stillCurrent) { - claim.cancel(); - } - return true; - } - synchronized (this) { - if (pendingForegroundRequest == target) { - pendingForegroundRequest = null; - } - } - if (claim.availability() == Leelaz.TrackingHandoffAvailability.NOT_TRACKING) { - return false; - } - foregroundLeaseAvailability = Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE; - target.fail(Leelaz.TrackingHandoffFailure.ACTIVATION_FAILED); - return true; - } - - private boolean activateForegroundRequest(ForegroundRequestTarget target) { - synchronized (this) { - if (pendingForegroundRequest != target || target.generation != foregroundRequestGeneration) { - return false; - } - pendingForegroundRequest = null; - sharedForegroundHandoffOwner = target; - sharedForegroundLeaseStarting = false; - sharedForegroundLeaseActive = true; - sharedForegroundRulesCapturePending = true; - foregroundLeaseAvailability = Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE; - } - if (target.kind == ForegroundRequestKind.ALL_BRANCHES) { - startRequestAllBranchesNow(target.showProgressDialog); - } else if (target.kind == ForegroundRequestKind.MISSING_MAINLINE) { - startRequestMissingMainlineNow(target.showProgressDialog); - } else if (target.kind == ForegroundRequestKind.WHOLE_GAME) { - startWholeGameRequestNow(target.requestedNodes, target.targetVisits, target.includeOwnership); - } else { - startRequestNow(target.startMove, target.endMove, target.showProgressDialog); - } - if (!requestDispatchFailed && analyzeMap.isEmpty()) { - finishSuccessfulEmptyRequest(); - return true; - } - if (!beginSharedForegroundRulesCapture()) { - requestDispatchFailed = true; - finishFailedRequestDispatch(target.showProgressDialog); - } - return true; - } private void finishSuccessfulEmptyRequestIfNeeded() { if (!requestDispatchFailed @@ -1362,38 +1228,10 @@ private void finishSuccessfulEmptyRequest() { } } - private void failForegroundRequest(ForegroundRequestTarget target) { - synchronized (this) { - if (pendingForegroundRequest == target) { - pendingForegroundRequest = null; - } - } - finishFailedRequestDispatch(target.showProgressDialog); - } - - private void closeForegroundHandoff(ForegroundRequestTarget target) { - synchronized (this) { - if (sharedForegroundHandoffOwner != target) { - return; - } - sharedForegroundHandoffOwner = null; - sharedForegroundLeaseActive = false; - sharedForegroundRulesCapturePending = false; - } - requestDispatchFailed = true; - finishFailedRequestDispatch(target.showProgressDialog); - } public int startRequestMissingMainline(boolean showProgressDialog) { if (!isLoaded || shutdownRequested) return -1; - int requestCount = countMissingMainlineRequests(); - if (requestCount > 0) { - ForegroundRequestTarget target = - ForegroundRequestTarget.missingMainline(this, showProgressDialog); - if (tryClaimForegroundTrackingHandoff(target)) { - return target.failed ? -1 : requestCount; - } - } + if (countMissingMainlineRequests() == 0) return 0; return startRequestMissingMainlineNow(showProgressDialog); } @@ -1446,14 +1284,7 @@ public int startWholeGameRequest( requestedNodes, targetVisits, includeOwnership && supportsWholeGameOwnershipRequests()); - if (!pendingNodes.isEmpty()) { - ForegroundRequestTarget target = - ForegroundRequestTarget.wholeGame( - this, pendingNodes, targetVisits, includeOwnership); - if (tryClaimForegroundTrackingHandoff(target)) { - return target.failed ? -1 : pendingNodes.size(); - } - } + if (pendingNodes.isEmpty()) return 0; return startWholeGameRequestNow(pendingNodes, targetVisits, includeOwnership); } @@ -1921,7 +1752,7 @@ private boolean startNextRemoteGtpJob() { if (sharedForegroundEngine != null && !sharedForegroundLeaseActive) { return beginSharedForegroundLease(); } - if (sharedForegroundEngine != null && sharedForegroundRulesCapturePending) { + if (sharedForegroundEngine != null && remoteGtpExpectedRulesCommandId > 0) { return true; } RemoteGtpAnalyzeJob job = remoteGtpQueue().pollFirst(); @@ -2155,27 +1986,19 @@ private synchronized boolean releaseSharedForegroundLease( return true; } Leelaz.ForegroundAnalysisLease lease = sharedForegroundLease; - ForegroundRequestTarget handoffOwner = sharedForegroundHandoffOwner; sharedForegroundLeaseStarting = false; sharedForegroundLeaseActive = false; sharedForegroundLease = null; - sharedForegroundHandoffOwner = null; - sharedForegroundRulesCapturePending = false; - if (lease == null && handoffOwner == null) { + if (lease == null) { return false; } sharedForegroundRestoreInProgress = true; sharedForegroundRestoreCompletion = afterRestore; sharedForegroundRestoreFailure = afterRestoreFailure; boolean restorePending = - lease != null - ? lease.release( - () -> finishTrackedSharedForegroundRestore(true), - () -> finishTrackedSharedForegroundRestore(false)) - : sharedForegroundEngine.endForegroundAnalysisLease( - handoffOwner, - () -> finishTrackedSharedForegroundRestore(true), - () -> finishTrackedSharedForegroundRestore(false)); + lease.release( + () -> finishTrackedSharedForegroundRestore(true), + () -> finishTrackedSharedForegroundRestore(false)); if (!restorePending && sharedForegroundRestoreInProgress) { sharedForegroundRestoreInProgress = false; sharedForegroundRestoreCompletion = null; @@ -2635,161 +2458,10 @@ public boolean isLocalDedicatedProcess() { && process.isAlive(); } - private enum ForegroundRequestKind { - MAINLINE, - ALL_BRANCHES, - MISSING_MAINLINE, - WHOLE_GAME - } - - private static final class ForegroundRequestTarget implements Leelaz.TrackingHandoffTarget { - private final AnalysisEngine owner; - private final BoardHistoryNode historyRoot; - private final BoardHistoryNode historyNode; - private final Zobrist boardPosition; - private final boolean blackToPlay; - private final long contextRevision; - private final ForegroundRequestKind kind; - private final int startMove; - private final int endMove; - private final boolean showProgressDialog; - private final List requestedNodes; - private final int targetVisits; - private final boolean includeOwnership; - private final java.util.concurrent.atomic.AtomicBoolean activationStarted = - new java.util.concurrent.atomic.AtomicBoolean(false); - private final java.util.concurrent.atomic.AtomicBoolean settled = - new java.util.concurrent.atomic.AtomicBoolean(false); - private volatile long generation; - private volatile Leelaz.TrackingHandoffClaim claim; - private volatile boolean failed; - - private ForegroundRequestTarget( - AnalysisEngine owner, - ForegroundRequestKind kind, - int startMove, - int endMove, - boolean showProgressDialog) { - this(owner, kind, startMove, endMove, showProgressDialog, null, -1, false); - } - - private ForegroundRequestTarget( - AnalysisEngine owner, - ForegroundRequestKind kind, - int startMove, - int endMove, - boolean showProgressDialog, - List requestedNodes, - int targetVisits, - boolean includeOwnership) { - this.owner = owner; - BoardHistoryList history = Lizzie.board == null ? null : Lizzie.board.getHistory(); - this.historyRoot = history == null ? null : history.getStart(); - this.historyNode = history == null ? null : history.getCurrentHistoryNode(); - this.boardPosition = history == null ? null : history.getZobrist(); - this.blackToPlay = history != null && history.isBlacksTurn(); - this.contextRevision = Lizzie.board == null ? 0L : Lizzie.board.getContextRevision(); - this.kind = kind; - this.startMove = startMove; - this.endMove = endMove; - this.showProgressDialog = showProgressDialog; - this.requestedNodes = requestedNodes; - this.targetVisits = targetVisits; - this.includeOwnership = includeOwnership; - } - - private static ForegroundRequestTarget mainline( - AnalysisEngine owner, int startMove, int endMove, boolean showProgressDialog) { - return new ForegroundRequestTarget( - owner, ForegroundRequestKind.MAINLINE, startMove, endMove, showProgressDialog); - } - - private static ForegroundRequestTarget allBranches( - AnalysisEngine owner, boolean showProgressDialog) { - return new ForegroundRequestTarget( - owner, ForegroundRequestKind.ALL_BRANCHES, -1, -1, showProgressDialog); - } - - private static ForegroundRequestTarget missingMainline( - AnalysisEngine owner, boolean showProgressDialog) { - return new ForegroundRequestTarget( - owner, ForegroundRequestKind.MISSING_MAINLINE, -1, -1, showProgressDialog); - } - - private static ForegroundRequestTarget wholeGame( - AnalysisEngine owner, - List requestedNodes, - int targetVisits, - boolean includeOwnership) { - return new ForegroundRequestTarget( - owner, - ForegroundRequestKind.WHOLE_GAME, - -1, - -1, - false, - List.copyOf(requestedNodes), - Math.max(1, targetVisits), - includeOwnership); - } - - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.FOREGROUND_ANALYSIS; - } - - @Override - public boolean isCurrent() { - synchronized (owner) { - return owner.pendingForegroundRequest == this - && owner.foregroundRequestGeneration == generation - && !owner.isNormalEnd - && owner.sharedForegroundEngine == Lizzie.leelaz - && Lizzie.board != null - && Lizzie.board.getHistory() != null - && Lizzie.board.getHistory().getStart() == historyRoot - && (kind == ForegroundRequestKind.WHOLE_GAME - || kind == ForegroundRequestKind.MISSING_MAINLINE - || (Lizzie.board.getHistory().getCurrentHistoryNode() == historyNode - && Lizzie.board.getHistory().getZobrist().equals(boardPosition) - && Lizzie.board.getHistory().isBlacksTurn() == blackToPlay - && Lizzie.board.getContextRevision() == contextRevision)); - } - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - if (!activationStarted.compareAndSet(false, true) || !isCurrent()) { - return; - } - if (!activation.activateForegroundAnalysis( - owner::parseLine, () -> owner.closeForegroundHandoff(this))) { - return; - } - settled.set(true); - if (!owner.activateForegroundRequest(this)) { - owner.sharedForegroundEngine.endForegroundAnalysisLease(this, null, null); - } - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - if (settled.compareAndSet(false, true)) { - failed = true; - owner.failForegroundRequest(this); - } - } - } void requestShutdown() { - ForegroundRequestTarget pendingTarget; synchronized (this) { shutdownRequested = true; - foregroundRequestGeneration++; - pendingTarget = pendingForegroundRequest; - pendingForegroundRequest = null; - } - if (pendingTarget != null && pendingTarget.claim != null) { - pendingTarget.claim.cancel(); } } @@ -2842,9 +2514,7 @@ public synchronized boolean isAnalysisInProgress() { /** Includes requests that are still acquiring or restoring a shared foreground-engine lease. */ public synchronized boolean hasRequestLifecycleInProgress() { - return pendingForegroundRequest != null - || sharedForegroundHandoffOwner != null - || sharedForegroundLeaseStarting + return sharedForegroundLeaseStarting || sharedForegroundLeaseActive || isAnalysisInProgress(); } diff --git a/src/main/java/featurecat/lizzie/analysis/EngineManager.java b/src/main/java/featurecat/lizzie/analysis/EngineManager.java index 4a76014e2..6f0750f76 100644 --- a/src/main/java/featurecat/lizzie/analysis/EngineManager.java +++ b/src/main/java/featurecat/lizzie/analysis/EngineManager.java @@ -3735,7 +3735,7 @@ private void updateEnginesWhileMarkedUpdating() throws JSONException, IOExceptio Board restoreBoard = Lizzie.board; boolean resumePonder = previousForegroundEngine != null - && previousForegroundEngine.isPonderingOrWasPonderingBeforeTracking(); + && previousForegroundEngine.isPondering(); Leelaz preparedTarget = null; Leelaz preparedMirror = null; InitialEngineStartupSynchronization lifecycleSynchronization = null; @@ -4487,7 +4487,7 @@ public void reStartEngine() { return; } Leelaz currentForegroundEngine = Lizzie.leelaz; - boolean restartPonderIntent = currentForegroundEngine.isPonderingOrWasPonderingBeforeTracking(); + boolean restartPonderIntent = currentForegroundEngine.isPondering(); int restartEngineIndex = currentEngineNo; if (rejectSameEngineSelection(restartEngineIndex, true)) return; Leelaz restartTarget = engineList.get(restartEngineIndex); @@ -4512,9 +4512,6 @@ public void reStartEngine() { if (reservations == null) { showForegroundEngineLeaseInUse(); return; - } - if (!attachRestartInteractionGate(reservations)) { - return; } shutdownEngineForRestart(restartTarget); switchEngineInternal( @@ -4541,10 +4538,6 @@ public void reStartEngine() { return; } lifecycleSynchronization.installReservations(reservations); - if (!lifecycleSynchronization.attachRestartInteractionGate()) { - showEngineSynchronizationFailure(restartTarget); - return; - } shutdownEngineForRestart(restartTarget); switchEngineInternal( restartEngineIndex, @@ -4557,7 +4550,6 @@ public void reStartEngine() { true, restartPonderIntent, lifecycleSynchronization::close, - lifecycleSynchronization.isTrackingFirstWinner(), preparedSwitch.lifecycleRestore)); } @@ -4571,7 +4563,7 @@ public void reStartEngine(int index) { } if (rejectSameEngineSelection(index, true)) return; Leelaz currentForegroundEngine = Lizzie.leelaz; - boolean restartPonderIntent = currentForegroundEngine.isPonderingOrWasPonderingBeforeTracking(); + boolean restartPonderIntent = currentForegroundEngine.isPondering(); Leelaz targetEngine = engineList.get(index); PreparedEngineSwitch preparedSwitch; try { @@ -4595,9 +4587,6 @@ public void reStartEngine(int index) { showForegroundEngineLeaseInUse(); return; } - if (!attachRestartInteractionGate(reservations)) { - return; - } shutdownEngineForRestart(targetEngine); switchEngineInternal( index, @@ -4623,10 +4612,6 @@ public void reStartEngine(int index) { return; } lifecycleSynchronization.installReservations(reservations); - if (!lifecycleSynchronization.attachRestartInteractionGate()) { - showEngineSynchronizationFailure(targetEngine); - return; - } shutdownEngineForRestart(targetEngine); switchEngineInternal( index, @@ -4639,7 +4624,6 @@ public void reStartEngine(int index) { true, restartPonderIntent, lifecycleSynchronization::close, - lifecycleSynchronization.isTrackingFirstWinner(), preparedSwitch.lifecycleRestore)); } @@ -4654,7 +4638,7 @@ public void reStartEngine2() { int restartEngineIndex = currentEngineNo2; if (rejectSameEngineSelection(restartEngineIndex, false)) return; boolean restartPonderIntent = - Lizzie.leelaz != null && Lizzie.leelaz.isPonderingOrWasPonderingBeforeTracking(); + Lizzie.leelaz != null && Lizzie.leelaz.isPondering(); Leelaz secondaryTarget = engineList.get(restartEngineIndex); PreparedEngineSwitch preparedSwitch; try { @@ -4715,7 +4699,6 @@ public void reStartEngine2() { true, restartPonderIntent, lifecycleSynchronization::close, - lifecycleSynchronization.isTrackingFirstWinner(), preparedSwitch.lifecycleRestore)); } @@ -11120,7 +11103,6 @@ private void prepareAndExecuteSubmittedEngineSwitch( false, false, releaseTransaction, - reservations.isTrackingFirstWinner(), null); Runnable uiAwareAfterSync = trackEngineSwitchUiCompletion( @@ -11169,7 +11151,6 @@ private void prepareAndExecuteSubmittedEngineSwitch( false, false, releaseLifecycle, - lifecycleSynchronization.isTrackingFirstWinner(), preparedSwitch.lifecycleRestore); Runnable uiAwareAfterSync = trackEngineSwitchUiCompletion( @@ -11400,7 +11381,6 @@ private Runnable releaseEngineLifecycleAfterBoardSync( explicitRestart, restartPonderIntent, reservations::close, - reservations.isTrackingFirstWinner(), lifecycleRestore); } @@ -11411,7 +11391,6 @@ private Runnable releaseEngineLifecycleAfterBoardSync( boolean explicitRestart, boolean restartPonderIntent, Runnable releaseLifecycle, - boolean trackingFirstWinner, PreparedLifecycleRestore lifecycleRestore) { // Re-selecting the exact foreground runtime is not an explicit restart or recovery. It may // converge a frozen Board route, but must not manufacture a ReadBoard recovery ACK for the @@ -11598,7 +11577,7 @@ private Runnable releaseEngineLifecycleAfterBoardSync( if (!isMain || current == null || target == null - || (!explicitRestart && !trackingFirstWinner && !readBoardRecovery)) { + || (!explicitRestart && !readBoardRecovery)) { return () -> { try { if (lifecycleRestore != null) { @@ -11677,7 +11656,8 @@ private void confirmLifecycleBoardSynchronization( if (lifecycleRestore != null) { lifecycleRestore.confirmBoardSynchronization(onSuccess, onFailure); } else { - target.confirmBoardSynchronization(onSuccess, detail -> onFailure.accept(target, detail)); + target.withCurrentRestartBootstrapReceipt( + () -> target.confirmBoardSynchronization(onSuccess, detail -> onFailure.accept(target, detail))).run(); } } @@ -12284,7 +12264,7 @@ private PreparedEngineSwitch prepareEngineSwitch( } boolean resumePonder = foregroundActivation - || (previousEngine != null && previousEngine.isPonderingOrWasPonderingBeforeTracking()); + || (previousEngine != null && previousEngine.isPondering()); Leelaz proposedRestoreMirror = Lizzie.config.isDoubleEngineMode() ? (isMain ? Lizzie.leelaz2 : Lizzie.leelaz) : null; if (proposedRestoreMirror != null && !proposedRestoreMirror.hasGtpCapability()) { @@ -14086,30 +14066,6 @@ private EngineLifecycleReservations reserveEngineLifecycle( return new EngineLifecycleReservations(currentReservation, targetReservation); } - private boolean attachRestartInteractionGate(EngineLifecycleReservations reservations) { - try { - if (reservations != null - && reservations.isTrackingFirstWinner() - && Lizzie.frame != null - && Lizzie.frame.isDisplayable()) { - reservations.interactionGate = Lizzie.frame.beginRestartInteractionGate(); - } - return true; - } catch (RuntimeException | Error failure) { - try { - reservations.close(); - } catch (RuntimeException | Error cleanupFailure) { - if (cleanupFailure != failure) { - failure.addSuppressed(cleanupFailure); - } - } - if (failure instanceof Error) { - throw (Error) failure; - } - showEngineSynchronizationFailure(Lizzie.leelaz); - return false; - } - } private static final class PreparedLifecycleRestore { private final Leelaz previousEngine; @@ -14304,7 +14260,8 @@ private static void confirmBoardSynchronization( onFailure.accept(engine, "engine was unavailable before board synchronization"); return; } - engine.confirmBoardSynchronization(onSuccess, detail -> onFailure.accept(engine, detail)); + engine.withCurrentRestartBootstrapReceipt( + () -> engine.confirmBoardSynchronization(onSuccess, detail -> onFailure.accept(engine, detail))).run(); } private Lizzie.PreparedEngineReadyPublication prepareAfterRestore( @@ -14319,7 +14276,8 @@ private Lizzie.PreparedEngineReadyPublication prepareAfterRestore( } private void initializeAfterExplicitRestart(boolean resumePonder) { - targetEngine.initializeAfterExplicitRestartBoardSynchronization(resumePonder); + targetEngine.withCurrentRestartBootstrapReceipt( + () -> targetEngine.initializeAfterExplicitRestartBoardSynchronization(resumePonder)).run(); } private void resumePonderAfterSuccessfulSynchronization() { @@ -14328,8 +14286,10 @@ private void resumePonderAfterSuccessfulSynchronization() { && targetEngine.isStarted() && targetEngine.isLoaded() && !targetEngine.isCheckingName) { - targetEngine.ponder(); - targetEngine.setResponseUpToDate(); + targetEngine.withCurrentRestartBootstrapReceipt(() -> { + targetEngine.ponder(); + targetEngine.setResponseUpToDate(); + }).run(); } } } @@ -14384,7 +14344,6 @@ private PreparedEngineSwitch( private static final class EngineLifecycleReservations implements AutoCloseable { private Leelaz.ExclusiveGtpLifecycleReservation current; private Leelaz.ExclusiveGtpLifecycleReservation target; - private LizzieFrame.RestartInteractionGate interactionGate; private EngineLifecycleReservations( Leelaz.ExclusiveGtpLifecycleReservation current, @@ -14393,22 +14352,16 @@ private EngineLifecycleReservations( this.target = target; } - private boolean isTrackingFirstWinner() { - return current != null && current.isTrackingFirstWinner(); - } @Override public void close() { Leelaz.ExclusiveGtpLifecycleReservation targetToClose; Leelaz.ExclusiveGtpLifecycleReservation currentToClose; - LizzieFrame.RestartInteractionGate gateToClose; synchronized (this) { targetToClose = target; currentToClose = current; - gateToClose = interactionGate; target = null; current = null; - interactionGate = null; } Throwable failure = null; if (targetToClose != null) { @@ -14417,9 +14370,6 @@ public void close() { if (currentToClose != null) { failure = runLifecycleCleanupStep(failure, currentToClose::close); } - if (gateToClose != null) { - failure = runLifecycleCleanupStep(failure, gateToClose::close); - } rethrowLifecycleCleanupFailure(failure); } } @@ -14590,13 +14540,11 @@ static final class InitialEngineStartupSynchronization implements AutoCloseable private final Object lifecycleOwner; private Leelaz.LifecycleCompletionClaim completionClaim; private EngineLifecycleReservations reservations; - private LizzieFrame.RestartInteractionGate interactionGate; private PreparedLifecycleRestore pendingRoute; private BoardFrame capturedFrame; private boolean stable; private boolean engineGameInitialization; private EngineGameOwnerTransaction engineGameTransaction; - private boolean trackingFirstWinner; private final AtomicBoolean barriersEnded = new AtomicBoolean(false); private final AtomicBoolean closed = new AtomicBoolean(false); private final CountDownLatch closeSettled = new CountDownLatch(1); @@ -15005,7 +14953,6 @@ private void installReservations(EngineLifecycleReservations preparedReservation synchronized (this) { if (!closed.get() && reservations == null) { reservations = preparedReservations; - trackingFirstWinner |= preparedReservations.isTrackingFirstWinner(); accepted = true; } } @@ -15027,34 +14974,11 @@ private void releaseReservation() { } } - private boolean isTrackingFirstWinner() { - return trackingFirstWinner; - } - - private boolean attachRestartInteractionGate() { - try { - if (trackingFirstWinner && Lizzie.frame != null && Lizzie.frame.isDisplayable()) { - interactionGate = Lizzie.frame.beginRestartInteractionGate(); - } - return true; - } catch (RuntimeException | Error failure) { - try { - close(); - } catch (RuntimeException | Error cleanupFailure) { - if (cleanupFailure != failure) { - failure.addSuppressed(cleanupFailure); - } - } - if (failure instanceof Error) { - throw (Error) failure; - } - return false; - } - } private void initializeAfterRestore() { if (initialized.compareAndSet(false, true)) { - Lizzie.initializeAfterVersionCheck(false, targetEngine); + targetEngine.withCurrentRestartBootstrapReceipt( + () -> Lizzie.initializeAfterVersionCheck(false, targetEngine)).run(); } } @@ -15140,9 +15064,6 @@ public void close() { try { Throwable failure = null; failure = runLifecycleCloseStep(failure, this::releaseReservation); - if (interactionGate != null) { - failure = runLifecycleCloseStep(failure, interactionGate::close); - } failure = runLifecycleCloseStep(failure, this::endSynchronizationBarriers); failure = runLifecycleCloseStep(failure, this::detachSynchronizationAdmission); Leelaz.LifecycleCompletionClaim claim = completionClaim; diff --git a/src/main/java/featurecat/lizzie/analysis/KataGoAnalysisPayload.java b/src/main/java/featurecat/lizzie/analysis/KataGoAnalysisPayload.java new file mode 100644 index 000000000..fd25be9f4 --- /dev/null +++ b/src/main/java/featurecat/lizzie/analysis/KataGoAnalysisPayload.java @@ -0,0 +1,78 @@ +package featurecat.lizzie.analysis; + +import featurecat.lizzie.rules.BoardData; +import java.util.ArrayList; +import java.util.List; + +/** One GTP analysis frame, before candidate display limits or cache admission. */ +public final class KataGoAnalysisPayload { + public final List moves; + public final int rootVisits; + public final double rootWinrate; + public final double rootScoreMean; + public final List ownership; + + private KataGoAnalysisPayload(List moves, int rootVisits, + double rootWinrate, double rootScoreMean, List ownership) { + this.moves = moves; + this.rootVisits = rootVisits; + this.rootWinrate = rootWinrate; + this.rootScoreMean = rootScoreMean; + this.ownership = ownership; + } + + public int totalVisits() { + return rootVisits >= 0 ? rootVisits : MoveData.getPlayouts(moves); + } + + public static KataGoAnalysisPayload parse(String line) { + MoveData.TokenCursor tokens = new MoveData.TokenCursor(line); + List moves = new ArrayList<>(); + int rootVisits = -1; + double rootWinrate = Double.NaN; + double rootScoreMean = Double.NaN; + List ownership = null; + while (tokens.next()) { + if (tokens.matches("info")) continue; + if (tokens.matches("move")) { + tokens.rewind(); + MoveData move = MoveData.fromInfoKatago(tokens); + moves.add(move); + } else if (tokens.matches("rootInfo")) { + while (tokens.next()) { + if (tokens.isTopLevelBoundary()) { + tokens.rewind(); + break; + } + int keyStart = tokens.start; + int keyEnd = tokens.end; + if (!tokens.next()) break; + if (tokens.isTopLevelBoundary()) { + tokens.rewind(); + break; + } + if (tokens.matches(tokens.value, keyStart, keyEnd, "visits")) { + rootVisits = tokens.fastInt(); + } else if (tokens.matches(tokens.value, keyStart, keyEnd, "winrate")) { + rootWinrate = tokens.fastDouble() * 100.0; + } else if (tokens.matches(tokens.value, keyStart, keyEnd, "scoreLead") + || tokens.matches(tokens.value, keyStart, keyEnd, "scoreMean")) { + rootScoreMean = tokens.fastDouble(); + } + } + } else if (tokens.matches("ownership") || tokens.matches("ownershipStdev")) { + boolean retain = tokens.matches("ownership"); + List values = retain ? new ArrayList<>() : null; + while (tokens.next()) { + if (tokens.isTopLevelBoundary()) { + tokens.rewind(); + break; + } + if (retain) values.add(tokens.fastDouble()); + } + if (retain) ownership = BoardData.compactEstimateArray(values); + } + } + return new KataGoAnalysisPayload(moves, rootVisits, rootWinrate, rootScoreMean, ownership); + } +} diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index 25f4b0f26..0b60ac9e5 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -53,6 +53,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.CountDownLatch; @@ -136,16 +137,6 @@ boolean preclear() { } } - public enum TrackingStreamLeaseFailure { - INITIAL_STOP_SEND_FAILED, - INITIAL_STOP_ERROR_RESPONSE, - INITIAL_STOP_TIMEOUT, - ACTIVE_COMMAND_SEND_FAILED, - FINAL_STOP_SEND_FAILED, - FINAL_STOP_ERROR_RESPONSE, - FINAL_STOP_TIMEOUT, - TRANSPORT_CLOSED - } private static final List FLASH_ANALYSIS_GTP_COMMANDS = List.of( @@ -239,81 +230,6 @@ private enum ExclusiveGtpWriteResult { SEND_FAILED } - private enum TrackingWriteState { - UNSENT, - WRITING, - SENT, - FAILED - } - - private enum ExclusiveGtpReleasePolicy { - FOREGROUND_RESTORE, - STREAM_ONLY - } - - public enum TrackingHandoffKind { - FOREGROUND_ANALYSIS, - RETAINED_ENGINE_MODE - } - - public enum TrackingHandoffAvailability { - ACCEPTED_PENDING, - BUSY, - NOT_TRACKING, - INVALID_TARGET - } - - public enum TrackingHandoffState { - ACCEPTED_PENDING, - ACTIVATING, - ACTIVE, - FAILED - } - - public enum TrackingHandoffFailure { - TRACKING_FAILED, - CONTEXT_INVALIDATED, - TARGET_CANCELLED, - ACTIVATION_FAILED - } - - public enum TrackingReleaseDisposition { - ACTIVE, - FROZEN_BY_SAFE, - CLEARED - } - - public enum TrackingReleaseReason { - SAFE_READ_ONLY_QUERY, - ORDINARY_OPERATION - } - - @FunctionalInterface - public interface TrackingReleaseDispositionObserver { - void onDispositionChanged(TrackingReleaseDisposition disposition); - - default void onReleaseClaimed(TrackingReleaseReason reason) {} - } - - public interface TrackingHandoffTarget { - TrackingHandoffKind kind(); - - boolean isCurrent(); - - void activate(TrackingHandoffActivation activation); - - void fail(TrackingHandoffFailure failure); - } - - public interface TrackingHandoffActivation { - boolean activateForegroundAnalysis(Consumer lineConsumer, Runnable onClosed); - - boolean completeRetainedEngineMode(); - - default EngineModeReservation beginRetainedEngineModeReservation() { - return null; - } - } // private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute private static final Runnable NO_OP_RESPONSE_HANDLER = () -> {}; @@ -425,7 +341,6 @@ default EngineModeReservation beginRetainedEngineModeReservation() { private int engineIncarnationLeaseDepth; private final IdentityHashMap engineIncarnationLeaseOwners = new IdentityHashMap<>(); - private volatile TrackingHandoffClaim trackingHandoffGate; private final ArrayDeque recentStdoutLines = new ArrayDeque(); private final ArrayDeque recentStderrLines = new ArrayDeque(); private volatile String loggingEngineId; @@ -672,7 +587,443 @@ public static boolean consumePendingIfDirectedTransfer( private volatile ReadBoardGmaResponseBinding readBoardGmaResponseBinding; private volatile boolean engineStateUnrestored; private volatile int currentTotalPlayouts; + private int currentRootVisits = -1; + private ParsedAnalysisInfo currentOrdinaryPayload; + private Object currentOrdinaryPayloadSource; public boolean supportMovesOwnership = false; + private boolean supportRootInfo; + + public enum MoveFocusCapability { UNKNOWN, PROBING, SUPPORTED, UNSUPPORTED } + + private volatile ReaderStreamBinding moveFocusCapabilityBinding; + private volatile MoveFocusCapability moveFocusCapability = MoveFocusCapability.UNKNOWN; + private volatile MoveFocusState moveFocusState; + private volatile MoveFocusResponse moveFocusBoundary; + private volatile String ordinaryFocusBaseCommand; + private volatile long moveFocusPauseGeneration; + private volatile MoveFocusProbe moveFocusProbe; + + private final class MoveFocusProbe { + final ReaderStreamBinding binding = currentReaderStreamBinding(); + final AnalysisInfoTarget target = captureAnalysisInfoTarget(); + final long generation = analysisOutputGeneration.get(); + final long pauseGeneration = moveFocusPauseGeneration; + final boolean addPlayer; + final boolean blackToPlay; + + MoveFocusProbe(boolean addPlayer, boolean blackToPlay) { + this.addPlayer = addPlayer; + this.blackToPlay = blackToPlay; + } + + boolean current() { + return moveFocusProbe == this && readerStreamBinding == binding && !binding.terminated + && generation == analysisOutputGeneration.get() && isCurrentAnalysisInfoTarget(target); + } + + void finish(MoveFocusCapability capability) { + if (!current()) { + if (moveFocusProbe == this && readerStreamBinding == binding && !binding.terminated) { + moveFocusBoundary = null; + moveFocusCapability = MoveFocusCapability.UNKNOWN; + trySendCommandFromQueue(); + } + return; + } + moveFocusBoundary = null; + synchronized (analysisControlPonderLock()) { + if (pauseGeneration == moveFocusPauseGeneration + && (Lizzie.frame == null || !Lizzie.frame.isUserAnalysisPaused())) { + ponderCurrentPosition(addPlayer, blackToPlay); + } + } + if (current()) moveFocusCapability = capability; + } + } + + private final class MoveFocusState { + final TrackingAnalysisController owner; + final TrackingAnalysisController.Context context; + final ReaderStreamBinding binding; + final AnalysisInfoTarget target; + final Object treeSource = new Object(); + final long generation; + final String baseCommand; + volatile Set desired = Set.of(); + volatile Set installed = Set.of(); + volatile long revision; + volatile boolean pendingAdoption; + volatile boolean adopted; + volatile boolean retired; + volatile boolean paused; + + MoveFocusState(TrackingAnalysisController owner, TrackingAnalysisController.Context context, + ReaderStreamBinding binding, AnalysisInfoTarget target, String baseCommand) { + this.owner = owner; + this.context = context; + this.binding = binding; + this.target = target; + this.generation = analysisOutputGeneration.get(); + this.baseCommand = baseCommand; + } + + boolean current() { + return moveFocusState == this && !retired && readerStreamBinding == binding + && !binding.terminated && generation == analysisOutputGeneration.get() + && isCurrentAnalysisInfoTarget(target); + } + } + + public MoveFocusCapability moveFocusCapability() { + return moveFocusCapabilityBinding == currentReaderStreamBinding() + ? moveFocusCapability : MoveFocusCapability.UNKNOWN; + } + + public boolean canStartMoveFocus() { + synchronized (engineArbitrationLock()) { + ReaderStreamBinding binding = currentReaderStreamBinding(); + AnalysisOutputOwnership outputOwner = binding.analysisOutputOwnership.get(); + return moveFocusCapabilityBinding == binding + && moveFocusCapability == MoveFocusCapability.SUPPORTED + && isEligibleLocalKataGoForReadBoardTracking() + && confirmedReadBoardTrackingIncarnation() == binding.incarnation + && ordinaryFocusBaseCommand != null + && outputOwner != null && outputOwner.isOrdinary() + && exclusiveGtpSession == null + && readBoardGmaReservation == null && !foregroundRestoreInProgress + && !isWebTrialEngineBusy() + && Lizzie.frame != null && !Lizzie.frame.isPlayingAgainstLeelaz + && !Lizzie.frame.isAnaPlayingAgainstLeelaz; + } + } + + List beginMoveFocus(TrackingAnalysisController owner, + TrackingAnalysisController.Context context, java.util.function.BooleanSupplier validator) { + if (!canStartMoveFocus()) return null; + ReaderStreamBinding binding = currentReaderStreamBinding(); + synchronized (engineArbitrationLock()) { + if (!canStartMoveFocus()) return null; + binding.analysisOutputMutationLock.lock(); + try { + if (binding != readerStreamBinding || binding.terminated + || (validator != null && !validator.getAsBoolean())) return null; + AnalysisInfoTarget target = captureAnalysisInfoTarget(); + if (!isCurrentAnalysisInfoTarget(target) + || context.engine() != this || context.engineIncarnation() != binding.incarnation + || context.displayNodeIdentity() != target.displayNode + || context.historyIdentity() != target.board.getHistory()) return null; + MoveFocusState state = moveFocusState; + if (state != null && state.current() && (state.owner != owner || state.context != context)) { + return null; + } + if (state == null || !state.current()) { + String base = ordinaryFocusBaseCommand; + if (base == null) base = "kata-analyze " + maybeAddPlayer() + getInterval() + addKataTag(); + state = new MoveFocusState(owner, context, binding, target, base); + moveFocusState = state; + } + state.pendingAdoption = true; + synchronized (analysisInfoMutationLock()) { + AnalysisOutputOwnership source = binding.analysisOutputOwnership.get(); + if (source != null && source.isOrdinary() + && currentOrdinaryPayload != null && currentOrdinaryPayload.moves == bestMoves + && currentOrdinaryPayloadSource == source + && analysisInfoPayloadTarget != null + && isCurrentAnalysisInfoTarget(analysisInfoPayloadTarget) + && analysisInfoPayloadTarget.displayNode == target.displayNode + && currentRootVisits >= 0 && !bestMoves.isEmpty() + && AnalysisCandidateValidator.allCandidatesOnEmptyPoints(bestMoves, target.displayNode.getData())) { + target.displayNode.getData().adoptOrdinaryAnalysis(new ArrayList<>(bestMoves), + bestMovesEnginename, this, currentRootVisits, currentRootVisits, + currentOrdinaryPayload.estimateArray, + state.treeSource, target.secondarySlot, true); + state.adopted = true; + state.pendingAdoption = false; + return bestMoves; + } + } + return List.of(); + } finally { + binding.analysisOutputMutationLock.unlock(); + } + } + } + + boolean updateMoveFocus(TrackingAnalysisController owner, + TrackingAnalysisController.Context context, Set points, long revision, + boolean retireUnconsumed) { + MoveFocusState state = moveFocusState; + if (state == null || state.owner != owner || state.context != context || !state.current()) return false; + state.binding.analysisOutputMutationLock.lock(); + try { + if (!state.current()) return false; + if (retireUnconsumed) state.pendingAdoption = false; + state.desired = Set.copyOf(points); + state.revision = revision; + } finally { + state.binding.analysisOutputMutationLock.unlock(); + } + cancelUnwrittenMoveFocusUpdate(state); + if (moveFocusBoundary != null) return true; + if (state.installed.equals(points)) { + owner.onFocusUpdateConfirmed(context, revision, points); + return true; + } + return sendMoveFocusStop(state, false); + } + + private boolean stopMoveFocusForPause() { + MoveFocusState state = moveFocusState; + if (state == null || !state.current()) return false; + boolean gainPending = !state.desired.isEmpty() || !state.installed.isEmpty(); + state.paused = true; + state.pendingAdoption = false; + state.desired = Set.of(); + cancelUnwrittenMoveFocusUpdate(state); + long revision = state.revision; + COMMAND_DISPATCH_EXECUTOR.execute( + () -> state.owner.onFocusStopping(state.context, revision, gainPending)); + if (moveFocusBoundary == null) sendMoveFocusStop(state, false); + return true; + } + + private void retireMoveFocusForStateChange() { + MoveFocusState state = moveFocusState; + if (state == null || state.retired) return; + state.pendingAdoption = false; + state.retired = true; + state.desired = Set.of(); + cancelUnwrittenMoveFocusUpdate(state); + COMMAND_DISPATCH_EXECUTOR.execute(() -> state.owner.onFocusStopped(state.context, true)); + } + + void retireMoveFocus(TrackingAnalysisController owner, TrackingAnalysisController.Context context) { + MoveFocusState state = moveFocusState; + if (state == null || state.owner != owner || state.context != context) return; + state.pendingAdoption = false; + state.retired = true; + state.desired = Set.of(); + cancelUnwrittenMoveFocusUpdate(state); + if (moveFocusBoundary == null && !state.installed.isEmpty()) sendMoveFocusStop(state, false); + } + + private void cancelUnwrittenMoveFocusUpdate(MoveFocusState state) { + MoveFocusResponse pending = moveFocusBoundary; + if (pending == null || pending.state != state || !pending.analyze || pending.probe + || pending.command == null) return; + QueuedCommand command = pending.command; + synchronized (commandQueue()) { + if (!command.cancelBeforeOutputWrite(new IllegalStateException("Focus selection changed"))) return; + pending.superseded = true; + commandQueue().remove(command); + if (command.claimCommandCountRetirement()) cmdNumber = Math.max(1, cmdNumber - 1); + if (currentCmdNum > cmdNumber - 1) currentCmdNum = cmdNumber - 1; + } + pending.finishSuperseded(); + } + + private void continueMoveFocusAfterStop(MoveFocusState state) { + state.installed = Set.of(); + if (state.current() && isPondering && !state.paused) { + String command = state.baseCommand; + if (!command.contains(" rootInfo ")) command += " rootInfo true"; + if (!state.desired.isEmpty()) { + command += " focus " + String.join(",", new java.util.TreeSet<>(state.desired)) + " 0.5"; + } + MoveFocusResponse next = new MoveFocusResponse(state, false, true); + moveFocusBoundary = next; + sendMoveFocusCommand(command, next); + } else { + moveFocusBoundary = null; + COMMAND_DISPATCH_EXECUTOR.execute(() -> state.owner.onFocusStopped(state.context, state.retired)); + trySendCommandFromQueue(); + } + } + + private boolean sendMoveFocusStop(MoveFocusState state, boolean probe) { + MoveFocusResponse response = new MoveFocusResponse(state, probe, false); + moveFocusBoundary = response; + return sendMoveFocusCommand("stop", response); + } + + private boolean sendMoveFocusCommand(String command, MoveFocusResponse response) { + boolean accepted = + sendCommand( + command, + response, + failure -> response.fail(), + true, + false, + null, + true, + response.binding); + if (!accepted) response.fail(); + return accepted; + } + + public boolean startMoveFocusProbeAfterInitialization() { + synchronized (analysisControlPonderLock()) { + if (Lizzie.frame != null && Lizzie.frame.isUserAnalysisPaused()) return false; + return startMoveFocusProbeWhenAnalysisAllowed(); + } + } + + private boolean startMoveFocusProbeWhenAnalysisAllowed() { + LifecycleCompletionClaim completing = lifecycleCompletionCommandContext.get(); + if (completing != null && completing == lifecycleCompletionClaim + && moveFocusCapability() == MoveFocusCapability.UNKNOWN && isKatago) { + ReaderStreamBinding binding = currentReaderStreamBinding(); + AnalysisInfoTarget target = captureAnalysisInfoTarget(); + long pauseGeneration = moveFocusPauseGeneration; + completing.runAfterEndpointRelease(() -> { + if (readerStreamBinding == binding && !binding.terminated + && isCurrentAnalysisInfoTarget(target) && pauseGeneration == moveFocusPauseGeneration) { + if (!startMoveFocusProbeAfterInitialization()) ponderIfAnalysisControlAllows(); + } + }); + return true; + } + if (moveFocusCapability() != MoveFocusCapability.UNKNOWN || isPondering + || !isEligibleLocalKataGoForReadBoardTracking() + || confirmedReadBoardTrackingIncarnation() < 0 || exclusiveGtpSession != null + || readBoardGmaReservation != null || foregroundRestoreInProgress) return false; + ReaderStreamBinding binding = currentReaderStreamBinding(); + if (moveFocusProbe != null && moveFocusProbe.binding == binding) return false; + synchronized (engineArbitrationLock()) { + synchronized (commandQueue()) { + if (moveFocusCapability() != MoveFocusCapability.UNKNOWN || normalCommandSendInProgress + || !commandQueue().isEmpty()) return false; + moveFocusCapabilityBinding = binding; + moveFocusProbe = new MoveFocusProbe(false, false); + moveFocusCapability = MoveFocusCapability.PROBING; + } + } + MoveFocusResponse probe = new MoveFocusResponse(null, true, true); + moveFocusBoundary = probe; + sendMoveFocusCommand("kata-analyze " + getInterval() + " rootInfo true focus pass 0", probe); + return true; + } + + private final class MoveFocusResponse implements Runnable { + final MoveFocusState state; + final ReaderStreamBinding binding; + final boolean probe; + final boolean analyze; + final MoveFocusProbe probeContext; + final Set points; + final long revision; + final Timer timeout = new Timer("lizzie-focus-response-timeout", true); + volatile String finalHeader; + volatile boolean failed; + volatile QueuedCommand command; + volatile boolean superseded; + final AtomicBoolean terminalClaimed = new AtomicBoolean(); + final AtomicBoolean supersessionHandled = new AtomicBoolean(); + + void finishSuperseded() { + timeout.cancel(); + if (supersessionHandled.compareAndSet(false, true) && moveFocusBoundary == this) { + if (probe) { + moveFocusBoundary = null; + if (readerStreamBinding == binding && !binding.terminated) { + moveFocusCapability = MoveFocusCapability.UNKNOWN; + } + trySendCommandFromQueue(); + } else { + continueMoveFocusAfterStop(state); + } + } + } + + boolean validBeforeWrite() { + return !analyze || (probe ? probeContext.current() + && probeContext.pauseGeneration == moveFocusPauseGeneration + : state.current() && !state.paused && state.revision == revision + && state.desired.equals(points)); + } + + MoveFocusResponse(MoveFocusState state, boolean probe, boolean analyze) { + this.state = state; + this.probeContext = probe ? moveFocusProbe : null; + this.binding = state == null ? probeContext.binding : state.binding; + this.probe = probe; + this.analyze = analyze; + this.points = state == null ? Set.of() : state.desired; + this.revision = state == null ? 0 : state.revision; + timeout.schedule(new TimerTask() { @Override public void run() { fail(); } }, 5000); + } + + void fail() { + if (!terminalClaimed.compareAndSet(false, true)) return; + failClaimed(); + } + + private void failClaimed() { + timeout.cancel(); + if (superseded) { finishSuperseded(); return; } + failed = true; + if (binding != readerStreamBinding || binding.terminated) return; + engineStateUnrestored = true; + isPondering = false; + invalidateAnalysisInfoPayloadForLocalStateChange(false); + if (state != null) { + state.pendingAdoption = false; + state.retired = true; + COMMAND_DISPATCH_EXECUTOR.execute(() -> state.owner.onFocusStopped(state.context, true)); + } + } + + @Override public void run() { + if (!terminalClaimed.compareAndSet(false, true)) return; + timeout.cancel(); + if (failed || superseded || binding != readerStreamBinding || binding.terminated) return; + boolean error = currentCommandResponseError; + if (error && !(probe && analyze)) { failClaimed(); return; } + if (probe && analyze) { + if (error) { + probeContext.finish(MoveFocusCapability.UNSUPPORTED); + } else { + sendMoveFocusStop(null, true); + } + return; + } + if (probe) { + supportRootInfo = true; + probeContext.finish(MoveFocusCapability.SUPPORTED); + return; + } + if (state == null) return; + if (analyze) { + state.installed = points; + moveFocusBoundary = null; + COMMAND_DISPATCH_EXECUTOR.execute(() -> { + state.owner.onFocusUpdateConfirmed(state.context, revision, points); + if (state.current() && !state.desired.equals(points)) sendMoveFocusStop(state, false); + }); + } else { + continueMoveFocusAfterStop(state); + } + } + } + + private boolean dispatchMoveFocusLine(ReaderStreamBinding binding, String line) { + MoveFocusResponse boundary = moveFocusBoundary; + if (boundary == null || boundary.binding != binding) return false; + if (boundary.failed) return true; + if (boundary.finalHeader != null) { + if (line.isEmpty()) processCommandResponseLine(boundary.finalHeader, binding); + return true; + } + if (line.startsWith("=") || line.startsWith("?")) { + PendingResponseHandler pending = peekPendingResponseHandler(line); + if (pending != null && pending.handler == boundary) { + if (!boundary.analyze || line.startsWith("?")) boundary.finalHeader = line; + else processCommandResponseLine(line, binding); + return true; + } + } + return boundary.probe; + } // private int refreshNumber=0; // private boolean isEstimating=true; @@ -864,6 +1215,7 @@ public void startEngine(int index) throws IOException { deferredEngineGameRecovery ? -1L : Lizzie.capturePrimaryEngineGeneration(this); canRestoreDymPda = false; supportMovesOwnership = false; + supportRootInfo = false; CommandLaunchHelper.LaunchSpec launchSpec = CommandLaunchHelper.prepare(Utils.splitCommand(engineCommand)); commands = launchSpec.getCommandParts(); @@ -1726,7 +2078,14 @@ public boolean isEligibleLocalKataGoForReadBoardTracking() { return this == Lizzie.leelaz && started && isLoaded - && trackingStaticAvailability() == ExclusiveGtpLeaseAvailability.AVAILABLE + && !useRemoteCompute + && !useJavaSSH + && !isSSH + && (Lizzie.config == null || !Lizzie.config.isDoubleEngineMode()) + && isKatago + && outputStream != null + && endGetCommandList + && commandLists.contains("kata-analyze") && !EngineManager.occupiesEngineGameAdmission(); } @@ -3774,19 +4133,15 @@ private void initializeStreams(InputStream stdout, OutputStream stdin, InputStre BufferedReader nextInputStream = new BufferedReader(new InputStreamReader(stdout)); BufferedOutputStream nextOutputStream = createCommandOutputStream(stdin); BufferedReader nextErrorStream = new BufferedReader(new InputStreamReader(stderr)); - ExclusiveGtpSession retiredTrackingSession = null; - TrackingHandoffFailureNotification retiredHandoffFailure = null; - TrackingDispositionNotification dispositionNotification = null; GtpCommandStateReset rebindCommandStateReset = null; boolean interrupted = false; - boolean ownsRebindGateAfterTrackingCleanup = false; boolean rebindCommandStateCutover = false; synchronized (engineArbitrationLock()) { if (engineIncarnationLeaseOwners.containsKey(Thread.currentThread())) { throw new IllegalStateException( "Cannot replace engine streams from an incarnation-lease owner"); } - while ((!ownsRebindGateAfterTrackingCleanup && readerStreamRebindInProgress) + while (readerStreamRebindInProgress || readerTerminalCleanupInProgress || isUpdateEngineStartCompletionClaimedLocked() || engineIncarnationLeaseDepth > 0 @@ -3794,21 +4149,7 @@ private void initializeStreams(InputStream stdout, OutputStream stdin, InputStre || (readerStreamBinding != null && readerStreamBinding.startupPostActionsInProgress > 0) || (readerStreamBinding != null - && readerStreamBinding.runtimeUiPresentationsInProgress > 0) - || isFailedTrackingStreamCleanupInProgress() - || isTrackingHandoffActivationCallbackInProgress()) { - if (!ownsRebindGateAfterTrackingCleanup - && (isFailedTrackingStreamCleanupInProgress() - || isTrackingHandoffActivationCallbackInProgress())) { - synchronized (commandQueue()) { - readerStreamRebindInProgress = true; - ownsRebindGateAfterTrackingCleanup = true; - if (isTrackingHandoffActivationCallbackInProgress()) { - claimTrackingHandoffFailureLocked( - trackingHandoffGate, TrackingHandoffFailure.TRACKING_FAILED); - } - } - } + && readerStreamBinding.runtimeUiPresentationsInProgress > 0)) { try { engineArbitrationLock().wait(); } catch (InterruptedException waitInterrupted) { @@ -3827,52 +4168,11 @@ private void initializeStreams(InputStream stdout, OutputStream stdin, InputStre } } retireAnalysisOutputBindingLocked(readerStreamBinding); - if (exclusiveGtpSession != null - && exclusiveGtpSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - if (exclusiveGtpSession.closing) { - retiredTrackingSession = exclusiveGtpSession; - rebindCommandStateReset = - resetGtpCommandStateForReaderRebindLocked( - "tracking stream retired after successful close boundary"); - } else { - TrackingStreamCleanup cleanup = - claimTrackingStreamCleanup( - exclusiveGtpSession, - TrackingStreamLeaseFailure.TRANSPORT_CLOSED, - "tracking stream retired before reader rebind", - true, - false); - if (cleanup != null) { - retiredTrackingSession = cleanup.session; - rebindCommandStateReset = cleanup.commandStateReset; - dispositionNotification = cleanup.dispositionNotification; - } else { - retiredTrackingSession = exclusiveGtpSession; - recordTrackingStreamLeaseFailure( - retiredTrackingSession, TrackingStreamLeaseFailure.TRANSPORT_CLOSED); - retiredTrackingSession.releaseStopFailed = true; - retiredTrackingSession.closing = true; - rebindCommandStateReset = - resetGtpCommandStateForReaderRebindLocked( - "stale tracking stream retired before reader rebind"); - } - } - if (dispositionNotification == null) { - dispositionNotification = - advanceTrackingReleaseDispositionLocked( - retiredTrackingSession, TrackingReleaseDisposition.CLEARED); - } - } else if (!readerStreamBinding.engineGameCommandStateRetired) { + if (!readerStreamBinding.engineGameCommandStateRetired) { rebindCommandStateReset = resetGtpCommandStateForReaderRebindLocked( "command state retired before reader rebind"); } - if (trackingHandoffGate != null) { - TrackingHandoffFailureSettlement handoffSettlement = - claimTrackingHandoffFailureLocked( - trackingHandoffGate, TrackingHandoffFailure.TRACKING_FAILED); - retiredHandoffFailure = handoffSettlement.notification; - } rebindCommandStateCutover = rebindCommandStateReset != null; } } @@ -3911,78 +4211,52 @@ private void initializeStreams(InputStream stdout, OutputStream stdin, InputStre synchronized (commandQueue()) { publishRestartBootstrapReceiptLocked(readerStreamBinding, nextOutputStream); } - if (ownsRebindGateAfterTrackingCleanup || readerStreamRebindInProgress) { + if (readerStreamRebindInProgress) { readerStreamRebindInProgress = false; engineArbitrationLock().notifyAll(); } } } if (rebindCommandStateCutover) { - if (retiredTrackingSession != null) { - cancelExclusiveGtpInitialStopTimeout(retiredTrackingSession); - cancelExclusiveGtpReleaseStopTimeout(retiredTrackingSession); - } - notifyTrackingDisposition(dispositionNotification); - try { - notifyGtpCommandStateReset(rebindCommandStateReset); - } finally { - Runnable onClosed = null; - synchronized (engineArbitrationLock()) { - if (retiredTrackingSession != null) { - if (exclusiveGtpSession == retiredTrackingSession) { - exclusiveGtpSession = null; - } - if (!retiredTrackingSession.closedCallbackRun) { - retiredTrackingSession.closedCallbackRun = true; - onClosed = retiredTrackingSession.onClosed; - } - } - inputStream = nextInputStream; - outputStream = nextOutputStream; - errorStream = nextErrorStream; - loadSgfResponseQuarantined = false; - ReaderStreamBinding nextBinding = - new ReaderStreamBinding( - nextInputStream, - nextErrorStream, - nextOutputStream, - process, - useRemoteCompute ? remoteTransport : null, - useJavaSSH ? javaSSH : null, - processIncarnationIds.incrementAndGet(), - startupPrimaryEngineGeneration, - isDeferredEngineGameRecoveryStartup(), - analysisOutputRecoveryTokenContext.get()); - nextBinding.rawOutput = stdin; - nextBinding.suppressGlobalEnginePresentation = - nextBinding.suppressGlobalEnginePresentation - || suppressGlobalEnginePresentationUntilOwned; - beforeReaderBindingPublicationForTest(); - synchronized (leela0110PonderStateLock) { - if (leela0110PonderingBinding == readerStreamBinding) { - clearLeela0110PonderStateLocked(); - } - readerStreamBinding = nextBinding; - if (suppressGlobalEnginePresentationUntilOwned) { - nextBinding.suppressGlobalEnginePresentation = true; - } + notifyGtpCommandStateReset(rebindCommandStateReset); + synchronized (engineArbitrationLock()) { + inputStream = nextInputStream; + outputStream = nextOutputStream; + errorStream = nextErrorStream; + loadSgfResponseQuarantined = false; + ReaderStreamBinding nextBinding = + new ReaderStreamBinding( + nextInputStream, + nextErrorStream, + nextOutputStream, + process, + useRemoteCompute ? remoteTransport : null, + useJavaSSH ? javaSSH : null, + processIncarnationIds.incrementAndGet(), + startupPrimaryEngineGeneration, + isDeferredEngineGameRecoveryStartup(), + analysisOutputRecoveryTokenContext.get()); + nextBinding.rawOutput = stdin; + nextBinding.suppressGlobalEnginePresentation = + nextBinding.suppressGlobalEnginePresentation + || suppressGlobalEnginePresentationUntilOwned; + beforeReaderBindingPublicationForTest(); + synchronized (leela0110PonderStateLock) { + if (leela0110PonderingBinding == readerStreamBinding) { + clearLeela0110PonderStateLocked(); } - recordUpdateEngineStartBindingLocked(readerStreamBinding); - synchronized (commandQueue()) { - publishRestartBootstrapReceiptLocked(readerStreamBinding, nextOutputStream); + readerStreamBinding = nextBinding; + if (suppressGlobalEnginePresentationUntilOwned) { + nextBinding.suppressGlobalEnginePresentation = true; } - readerStreamRebindInProgress = false; - engineArbitrationLock().notifyAll(); } - notifyTrackingHandoffFailure(retiredHandoffFailure); - try { - trySendCommandFromQueue(); - } catch (RuntimeException ex) { - ex.printStackTrace(); + recordUpdateEngineStartBindingLocked(readerStreamBinding); + synchronized (commandQueue()) { + publishRestartBootstrapReceiptLocked(readerStreamBinding, nextOutputStream); } - runTrackingCallback(onClosed); + readerStreamRebindInProgress = false; + engineArbitrationLock().notifyAll(); } - } else if (ownsRebindGateAfterTrackingCleanup) { try { trySendCommandFromQueue(); } catch (RuntimeException ex) { @@ -3995,6 +4269,7 @@ private void initializeStreams(InputStream stdout, OutputStream stdin, InputStre noteEngineStarted(); } + private void noteEngineStarted() { try { String id = EngineObservation.identityFor(this); @@ -4060,16 +4335,7 @@ private String currentObservationIdentity() { return id != null ? id : EngineObservation.identityFor(this); } - private boolean isFailedTrackingStreamCleanupInProgress() { - return exclusiveGtpSession != null - && exclusiveGtpSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && exclusiveGtpSession.closing - && exclusiveGtpSession.releaseStopFailed; - } - private boolean isTrackingHandoffActivationCallbackInProgress() { - return trackingHandoffGate != null && trackingHandoffGate.activationCallbackInProgress; - } private boolean isUpdateEngineStartCompletionClaimedLocked() { return activeUpdateEngineStartAttempt != null @@ -4114,11 +4380,12 @@ private ReaderStreamBinding currentReaderStreamBinding() { private void publishRestartBootstrapReceiptLocked( ReaderStreamBinding binding, BufferedOutputStream bindingOutput) { if (!exclusiveGtpLifecycleTransition - || !exclusiveGtpLifecycleQueueGate + || binding.analysisOutputRecoveryToken != null || exclusiveGtpLifecycleOwner == null) { restartBootstrapReceipt = null; return; } + exclusiveGtpLifecycleQueueGate = true; RestartBootstrapReceipt receipt = new RestartBootstrapReceipt( this, @@ -4231,49 +4498,28 @@ public long trackingStreamIncarnation() { return currentReaderStreamBinding().incarnation; } - public boolean restorePonderAfterTracking(TrackingStreamLeaseReceipt receipt) { - boolean claimed = false; - boolean restored = false; - try { - synchronized (engineArbitrationLock()) { - synchronized (commandQueue()) { - if (receipt == null - || receipt.engine() != this - || !receipt.wasPondering() - || Lizzie.leelaz != this - || !isLoaded() - || !isStarted() - || currentReaderStreamBinding().incarnation != receipt.engineIncarnation() - || exclusiveGtpSession != null - || trackingHandoffGate != null - || exclusiveGtpLifecycleTransition - || foregroundRestoreInProgress - || normalCommandSendInProgress - || !commandQueue().isEmpty() - || !foregroundRestoreCommandQueue().isEmpty()) { - return false; - } - claimed = true; - int commandNumberBeforePonder = cmdNumber; - if (!ponderIfAnalysisControlAllows()) { - return false; - } - if (cmdNumber > commandNumberBeforePonder) { - settleTrackingPonderResponseWatermark(); - } - restored = true; - } - } - } catch (Throwable ignored) { - // A failed ponder handback cannot own recovery or strand the ordinary writer. - } finally { - if (claimed) { - trySendCommandFromQueue(); + long confirmedReadBoardTrackingIncarnation() { + synchronized (engineArbitrationLock()) { + synchronized (commandQueue()) { + ReaderStreamBinding binding = currentReaderStreamBinding(); + AnalysisStateLineage lineage = + binding.queuedAnalysisStateLineage != null + ? binding.queuedAnalysisStateLineage + : binding.analysisStateLineage; + if (!isEligibleLocalKataGoForReadBoardTracking() + || binding.terminated + || engineStateUnrestored + || exclusiveGtpLifecycleTransition + || hasLifecycleCompletionLocked() + || lineage.isFailed() + || lineage.hasPendingResponses() + || lineage.pendingRestoreOwners.get() > 0) return -1L; + return binding.incarnation; } } - return restored; } + private boolean isCurrentReaderStreamBinding(ReaderStreamBinding binding) { ReaderStreamBinding current = readerStreamBinding; return current == binding && !binding.terminated; @@ -4776,23 +5022,7 @@ public List parseInfo(String line) { } public List parseInfoKatago(String line) { - int ownershipIndex = line.indexOf("ownership"); - if (ownershipIndex >= 0) { - line = line.substring(0, ownershipIndex); - } - List bestMoves = new ArrayList<>(); - String[] variations = line.split(" info "); - // int k = (Lizzie.config.limitMaxSuggestion > 0&&!Lizzie.config.showNoSuggCircle ? - // Lizzie.config.limitMaxSuggestion : 361); - for (String var : variations) { - if (!var.trim().isEmpty()) { - bestMoves.add(MoveData.fromInfoKatago(var)); - // k = k - 1; - // if (k < 1) - // break; - } - } - return bestMoves; + return KataGoAnalysisPayload.parse(line).moves; } private static final class ParsedAnalysisInfo { @@ -4800,16 +5030,18 @@ private static final class ParsedAnalysisInfo { private final int totalPlayouts; private final List estimateArray; private final boolean kata; + private final int rootVisits; private ParsedAnalysisInfo( List moves, int totalPlayouts, List estimateArray, - boolean kata) { + boolean kata, int rootVisits) { this.moves = List.copyOf(moves); this.totalPlayouts = totalPlayouts; this.estimateArray = estimateArray == null ? null : List.copyOf(estimateArray); this.kata = kata; + this.rootVisits = rootVisits; } } @@ -4885,37 +5117,19 @@ private ParsedAnalysisInfo parseAnalysisInfoPayload(String payload) { List parsedMoves; List estimateArray = null; if (kata) { - parsedMoves = parseInfoKatago(payload); - estimateArray = parseKataOwnershipEstimate(payload); + KataGoAnalysisPayload parsed = KataGoAnalysisPayload.parse(payload); + return new ParsedAnalysisInfo( + parsed.moves, parsed.totalVisits(), + Lizzie.config.showKataGoEstimate ? parsed.ownership : null, true, parsed.rootVisits); } else if (isSai) { parsedMoves = parseInfoSai(payload); } else { parsedMoves = parseInfo(payload); } return new ParsedAnalysisInfo( - parsedMoves, MoveData.getPlayouts(parsedMoves), estimateArray, kata); + parsedMoves, MoveData.getPlayouts(parsedMoves), estimateArray, kata, -1); } - private List parseKataOwnershipEstimate(String payload) { - if (!Lizzie.config.showKataGoEstimate) { - return null; - } - List estimateArray = new ArrayList<>(); - int ownershipIndex = payload.indexOf("ownership"); - if (ownershipIndex < 0) { - return estimateArray; - } - String ownership = payload.substring(ownershipIndex + "ownership".length()).trim(); - if (ownership.isEmpty()) { - return estimateArray; - } - for (String value : ownership.split(" ")) { - if (!value.isEmpty()) { - estimateArray.add(Double.parseDouble(value)); - } - } - return estimateArray; - } /** * Publishes an immutable accepted payload through a mutable BoardData copy. BoardData sorts and @@ -4923,8 +5137,8 @@ private List parseKataOwnershipEstimate(String payload) { * Caller holds {@link #analysisInfoMutationLock} and the exact analysis-output admission. */ private void publishAnalysisInfoToDisplay( - ParsedAnalysisInfo parsed, AnalysisInfoTarget target) { - publishAnalysisDisplayNonFatal(() -> publishAnalysisInfoToDisplayUnsafe(parsed, target)); + ParsedAnalysisInfo parsed, AnalysisInfoTarget target, Object source) { + publishAnalysisDisplayNonFatal(() -> publishAnalysisInfoToDisplayUnsafe(parsed, target, source)); } private void publishAnalysisDisplayNonFatal(Runnable publication) { @@ -4940,7 +5154,7 @@ private void publishAnalysisDisplayNonFatal(Runnable publication) { } private void publishAnalysisInfoToDisplayUnsafe( - ParsedAnalysisInfo parsed, AnalysisInfoTarget target) { + ParsedAnalysisInfo parsed, AnalysisInfoTarget target, Object source) { if (parsed == null || parsed.moves.isEmpty() || target == null @@ -4967,26 +5181,17 @@ private void publishAnalysisInfoToDisplayUnsafe( return; } List boardMoves = new ArrayList<>(parsed.moves); - if (secondaryDisplay) { - if (parsed.kata) { - displayData.tryToSetBestMoves2FromEngine( - boardMoves, - bestMovesEnginename, - this, - parsed.totalPlayouts, - parsed.estimateArray); - } else { - displayData.tryToSetBestMoves2FromEngine( - boardMoves, bestMovesEnginename, this, parsed.totalPlayouts, null); - } - } else if (parsed.kata) { - displayData.tryToSetBestMovesFromEngine( - boardMoves, - bestMovesEnginename, - this, - parsed.totalPlayouts, + if (parsed.kata) { + displayData.adoptOrdinaryAnalysis( + boardMoves, bestMovesEnginename, this, parsed.totalPlayouts, parsed.rootVisits, parsed.estimateArray, - false); + moveFocusState != null && moveFocusState.current() ? moveFocusState.treeSource : source, + secondaryDisplay, + moveFocusState != null && moveFocusState.current() + && (moveFocusState.pendingAdoption || moveFocusState.adopted)); + } else if (secondaryDisplay) { + displayData.tryToSetBestMoves2FromEngine( + boardMoves, bestMovesEnginename, this, parsed.totalPlayouts, null); } else { displayData.tryToSetBestMovesFromEngine( boardMoves, bestMovesEnginename, this, parsed.totalPlayouts, null, false); @@ -4994,16 +5199,45 @@ private void publishAnalysisInfoToDisplayUnsafe( } /** Caller holds {@link #analysisInfoMutationLock}; {@link #bestMoves} is the release fence. */ - private void publishParsedAnalysisInfoLocked( - ParsedAnalysisInfo parsed, AnalysisInfoTarget target) { + private boolean publishParsedAnalysisInfoLocked( + ParsedAnalysisInfo parsed, AnalysisInfoTarget target, Object source) { + MoveFocusState activeFocus = moveFocusState; + if (activeFocus != null && activeFocus.current()) { + if (activeFocus.paused) return false; + if (parsed.rootVisits < 0) { + activeFocus.pendingAdoption = false; + activeFocus.paused = true; + isPondering = false; + COMMAND_DISPATCH_EXECUTOR.execute(() -> { + if (activeFocus.current()) stopMoveFocusForPause(); + }); + return false; + } + } if (parsed.kata && !parsed.moves.isEmpty()) { scoreMean = parsed.moves.get(0).scoreMean; scoreStdev = parsed.moves.get(0).scoreStdev; } - publishAnalysisInfoToDisplay(parsed, target); + publishAnalysisInfoToDisplay(parsed, target, source); analysisInfoPayloadTarget = target; currentTotalPlayouts = parsed.totalPlayouts; + currentRootVisits = parsed.rootVisits; bestMoves = parsed.moves; + currentOrdinaryPayload = parsed; + currentOrdinaryPayloadSource = source; + MoveFocusState focus = moveFocusState; + if (focus != null && focus.current() && parsed.rootVisits >= 0 + && AnalysisCandidateValidator.allCandidatesOnEmptyPoints(parsed.moves, target.displayNode.getData())) { + if (focus.pendingAdoption) { + focus.adopted = true; + focus.pendingAdoption = false; + } + List accepted = parsed.moves; + COMMAND_DISPATCH_EXECUTOR.execute(() -> { + if (focus.current() && !focus.paused) focus.owner.onFocusAnalysis(focus.context, accepted); + }); + } + return true; } // 试下诊断:限频打印 katago info 落到哪个 displayNode、引擎首选 winrate。开关见 TrialDiag。 @@ -5151,7 +5385,7 @@ && parseResponseCommandId(line) != NO_RESPONSE_COMMAND_ID) { || !isCurrentAnalysisInfoTarget(analysisInfoTarget)) { return; } - publishParsedAnalysisInfoLocked(parsedInfo, analysisInfoTarget); + if (!publishParsedAnalysisInfoLocked(parsedInfo, analysisInfoTarget, ingressRoute.ownerToken)) return; infoCommitted.set(true); } }); @@ -5514,6 +5748,7 @@ private boolean publishExactZenEngineGameBestMoves( false)); analysisOutputGeneration.incrementAndGet(); currentTotalPlayouts = 0; + currentRootVisits = -1; analysisInfoPayloadTarget = null; bestMoves = List.of(); analysisInfoEpoch++; @@ -5712,9 +5947,14 @@ private StartupCommandAction checkNameAndVersionForCurrentReader( if (Integer.parseInt(ver[0]) > 1 || Integer.parseInt(ver[1]) > 10) { supportMovesOwnership = true; } + int major = Integer.parseInt(ver[0]); + int minor = Integer.parseInt(ver[1]); + int patch = ver.length > 2 ? Integer.parseInt(ver[2].split("[^0-9]", 2)[0]) : 0; + supportRootInfo = major > 1 || (major == 1 && (minor > 16 || (minor == 16 && patch >= 4))); } catch (Exception ex) { ex.printStackTrace(); supportMovesOwnership = false; + supportRootInfo = false; } } isCheckingVersion = false; @@ -6744,7 +6984,10 @@ private void resetAnalysisInfoPayload(boolean resetScore) { scoreStdev = 0; } currentTotalPlayouts = 0; + currentRootVisits = -1; analysisInfoPayloadTarget = null; + currentOrdinaryPayload = null; + currentOrdinaryPayloadSource = null; bestMoves = List.of(); analysisInfoEpoch++; } @@ -6756,6 +6999,7 @@ private void resetAnalysisInfoPayload(boolean resetScore) { * physically written analysis command can publish an owner for the new generation. */ private void invalidateAnalysisInfoPayloadForLocalStateChange(boolean resetScore) { + retireMoveFocusForStateChange(); analysisOutputGeneration.incrementAndGet(); resetAnalysisInfoPayload(resetScore); } @@ -6769,6 +7013,7 @@ private void applyAnalysisStateMutation(AnalysisStateMutation mutation) { if (mutation == null || mutation == AnalysisStateMutation.NONE) { return; } + retireMoveFocusForStateChange(); analysisOutputGeneration.incrementAndGet(); if (mutation == AnalysisStateMutation.RETAIN_PAYLOAD) { advanceAnalysisInfoEpoch(); @@ -6822,7 +7067,7 @@ private AnalysisInfoMutationOutcome commitAnalysisInfoLine( || !isCurrentAnalysisInfoTarget(analysisInfoTarget)) { return; } - publishParsedAnalysisInfoLocked(parsedInfo, analysisInfoTarget); + if (!publishParsedAnalysisInfoLocked(parsedInfo, analysisInfoTarget, route.ownerToken)) return; outcome.parsedInfo = parsedInfo; outcome.acceptedSnapshot = new AnalysisInfoSnapshot( @@ -6918,6 +7163,7 @@ private boolean clearOutOfDateAnalysisInfo(AnalysisOutputRoute route) { synchronized (analysisInfoMutationLock()) { if (Lizzie.config.isAutoAna) { currentTotalPlayouts = 0; + currentRootVisits = -1; analysisInfoPayloadTarget = null; bestMoves = List.of(); analysisInfoEpoch++; @@ -7036,6 +7282,7 @@ private boolean handleLeela0110AnalysisSignal( }); } currentTotalPlayouts = completedPlayouts; + currentRootVisits = -1; analysisInfoPayloadTarget = analysisInfoTarget; bestMoves = completedMoves; acceptedSnapshot.set( @@ -8095,20 +8342,21 @@ public void genmoveResign(boolean needPass) { synchronized (analysisInfoMutationLock()) { List acceptedMoves = bestMoves; if (!acceptedMoves.isEmpty()) { - int acceptedPlayouts = MoveData.getPlayouts(acceptedMoves); - currentTotalPlayouts = acceptedPlayouts; + int acceptedPlayouts = currentTotalPlayouts; + int acceptedRootVisits = currentRootVisits; publishAnalysisDisplayNonFatal( - () -> - Lizzie.board - .getHistory() - .getData() - .tryToSetBestMovesFromEngine( - new ArrayList<>(acceptedMoves), - bestMovesEnginename, - this, - acceptedPlayouts, - null, - false)); + () -> { + BoardData data = Lizzie.board.getHistory().getData(); + if (isKatago) { + data.adoptOrdinaryAnalysis( + new ArrayList<>(acceptedMoves), bestMovesEnginename, this, + acceptedPlayouts, acceptedRootVisits, null, null, false, false); + } else { + data.tryToSetBestMovesFromEngine( + new ArrayList<>(acceptedMoves), bestMovesEnginename, this, + acceptedPlayouts, null, false); + } + }); } } this.resigned = true; @@ -8372,8 +8620,10 @@ private void markEngineGameAnalysisTerminal(AnalysisGameTerminal terminal) { } public void nameCmd() { - if (isKatago) sendCommand("stop"); - else sendCommand("name"); + if (!stopMoveFocusForPause()) { + if (isKatago) sendCommand("stop"); + else sendCommand("name"); + } LizzieFrame.menu.toggleEngineMenuStatus(false, false); } @@ -8774,6 +9024,7 @@ && isCurrentAnalysisInfoTarget(analysisInfoTargetAtParse)) { updatedMoves.add(mv); List publishedMoves = List.copyOf(updatedMoves); currentTotalPlayouts = MoveData.getPlayouts(publishedMoves); + currentRootVisits = -1; analysisInfoPayloadTarget = analysisInfoTargetAtParse; bestMoves = publishedMoves; } @@ -8825,6 +9076,7 @@ && isCurrentAnalysisInfoTarget(analysisInfoTargetAtParse) false)); List publishedMoves = List.copyOf(updatedMoves); currentTotalPlayouts = updatedPlayouts; + currentRootVisits = -1; analysisInfoPayloadTarget = analysisInfoTargetAtParse; bestMoves = publishedMoves; } @@ -9040,22 +9292,6 @@ private void playPassInEngineGame(EngineManager.EngineGamePrimaryContext game) { } } - boolean hasTrackingStreamSession() { - synchronized (engineArbitrationLock()) { - return isTrackingStreamSession(exclusiveGtpSession); - } - } - - public boolean isPonderingOrWasPonderingBeforeTracking() { - synchronized (engineArbitrationLock()) { - return isPondering() - || (exclusiveGtpSession != null - && exclusiveGtpSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && exclusiveGtpSession.owner instanceof TrackingStreamLease - && !exclusiveGtpSession.closedCallbackRun - && exclusiveGtpSession.wasPondering); - } - } private enum StatefulOrdinaryMutationKind { NONE, @@ -9194,7 +9430,6 @@ private long admitStatefulOrdinaryCommandsLocked( for (String command : commands) { if (!canAdmitOrdinaryCommandLocked( command, - TrackingReleaseReason.ORDINARY_OPERATION, true, commandBinding, null, @@ -9205,7 +9440,6 @@ private long admitStatefulOrdinaryCommandsLocked( if (mirroredEngine != null && !mirroredEngine.canAdmitOrdinaryCommandLocked( command, - TrackingReleaseReason.ORDINARY_OPERATION, true, mirroredCommandBinding, null, @@ -9261,7 +9495,6 @@ private void enqueueStatefulOrdinaryCommandsLocked( null, false, settlement, - TrackingReleaseReason.ORDINARY_OPERATION, true, false, commandBinding, @@ -9562,6 +9795,11 @@ private void read(ReaderStreamBinding binding) { } lineInProgress = true; rememberRecentLine(recentStdoutLines, line); + if (dispatchMoveFocusLine(binding, line)) { + lineInProgress = false; + endReaderLine(binding); + continue; + } if (dispatchExclusiveGtpLine(binding, line)) { lineInProgress = false; endReaderLine(binding); @@ -9753,38 +9991,11 @@ private String finishTerminatedReaderIncarnation(ReaderStreamBinding binding) { interruptedForegroundWork = exclusiveGtpSession != null ? exclusiveGtpSession : foregroundRestoreSession; } - if (interruptedForegroundWork != null - && interruptedForegroundWork.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - TrackingStreamCleanup cleanup = - claimTrackingStreamCleanup( - interruptedForegroundWork, - TrackingStreamLeaseFailure.TRANSPORT_CLOSED, - "tracking stream transport closed", - false, - true); - if (cleanup != null) { - cancelExclusiveGtpInitialStopTimeout(interruptedForegroundWork); - cancelExclusiveGtpReleaseStopTimeout(interruptedForegroundWork); - try { - notifyTrackingDisposition(cleanup.dispositionNotification); - notifyGtpCommandStateReset(cleanup.commandStateReset); - } finally { - closeStreamOnlyExclusiveGtpSession(interruptedForegroundWork, false, true); - } - } else { - closeStreamOnlyExclusiveGtpSession(interruptedForegroundWork, false, true); - } - } else { - recordForegroundAnalysisLeaseFailure( - interruptedForegroundWork, ForegroundAnalysisLeaseFailure.TRANSPORT_CLOSED); - markForegroundRestoreFailed(interruptedForegroundWork, "engine transport closed"); - abortExclusiveGtpSession(); - } - if (interruptedForegroundWork == null - || interruptedForegroundWork.releasePolicy - == ExclusiveGtpReleasePolicy.FOREGROUND_RESTORE) { - completeForegroundRestore(interruptedForegroundWork); - } + recordForegroundAnalysisLeaseFailure( + interruptedForegroundWork, ForegroundAnalysisLeaseFailure.TRANSPORT_CLOSED); + markForegroundRestoreFailed(interruptedForegroundWork, "engine transport closed"); + abortExclusiveGtpSession(); + completeForegroundRestore(interruptedForegroundWork); ReadBoard readBoard = Lizzie.frame == null ? null : Lizzie.frame.readBoard; if (readBoard == null || !readBoard.failReadBoardGmaSessionForEngineTermination( @@ -9803,8 +10014,7 @@ private String finishTerminatedReaderIncarnation(ReaderStreamBinding binding) { if (binding.remoteTransport != null) { rememberRecentLine( recentStderrLines, - "Remote engine-game participant retired; recovery deferred until transaction" - + " retirement"); + "Remote engine-game participant retired; recovery deferred until transaction retirement"); } return null; } @@ -9821,15 +10031,13 @@ private String finishTerminatedReaderIncarnation(ReaderStreamBinding binding) { } if (!binding.normalExitRequested && !tryRecoverBundledOpenClNativeExit(binding.process)) { isDownWithError = true; - // isLoaded=false; return buildEngineExitDiagnostic( Lizzie.resourceBundle.getString("Leelaz.engineEndUnormalHint")); - // ("打开Gtp窗口(快捷键E)查看报错信息"); - // LizzieFrame.openMoreEngineDialog(); } return null; } + private void shutdownReaderTransport(ReaderStreamBinding binding) { ReaderExecutorSnapshot executors = requestReaderShutdown(binding); cancelLeela0110PonderForReaderBinding(binding); @@ -10129,7 +10337,6 @@ private void sendStartupPostActionCommand( onSendFailure, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, delivery, false, binding); @@ -10616,6 +10823,7 @@ void processCommandResponseLineForTest(String line) { void dispatchReaderLineForTest(String line) throws IOException { ReaderStreamBinding binding = currentReaderStreamBinding(); + if (dispatchMoveFocusLine(binding, line)) return; if (shouldQuarantineUnmatchedStrictResponseCarrier(line, binding)) { isCommandLine = false; return; @@ -10666,6 +10874,7 @@ void setBestMovesForEngineGameTest(List moves) { synchronized (analysisInfoMutationLock()) { List publishedMoves = moves == null ? List.of() : List.copyOf(moves); currentTotalPlayouts = MoveData.getPlayouts(publishedMoves); + currentRootVisits = -1; analysisInfoPayloadTarget = captureAnalysisInfoTarget(); bestMoves = publishedMoves; analysisInfoEpoch++; @@ -10682,35 +10891,10 @@ boolean runPendingResponseHandlerForTest(String line) { } public boolean sendRawConsoleCommand(String command) { - synchronized (engineArbitrationLock()) { - if (isTrackingStreamSession(exclusiveGtpSession) && !isSafeRawGtpQuery(command)) { - return false; - } - } - return sendCommand( - command, null, null, false, true, TrackingReleaseReason.SAFE_READ_ONLY_QUERY, null, false); + return sendCommand(command, null, null, false, true); } - private static boolean isSafeRawGtpQuery(String command) { - if (command == null || command.indexOf('\n') >= 0 || command.indexOf('\r') >= 0) { - return false; - } - String trimmed = command.trim(); - if (trimmed.isEmpty()) { - return false; - } - String[] tokens = trimmed.split("\\s+"); - String name = tokens[0].toLowerCase(Locale.ROOT); - if (name.equals("known_command")) { - return tokens.length == 2; - } - return tokens.length == 1 - && (name.equals("name") - || name.equals("version") - || name.equals("protocol_version") - || name.equals("list_commands") - || name.equals("showboard")); - } + private void sendCommand(String command, Runnable onResponse) { sendCommand(command, onResponse, null, false, true); @@ -10733,7 +10917,6 @@ private boolean sendCommand( onSendFailure, failOnSendError, mirrorToSecondEngine, - TrackingReleaseReason.ORDINARY_OPERATION, null, false, null); @@ -10745,7 +10928,6 @@ private boolean sendCommand( CommandSendFailureHandler onSendFailure, boolean failOnSendError, boolean mirrorToSecondEngine, - TrackingReleaseReason releaseReason, QueuedCommandSettlement settlement, boolean rejectForExclusiveWinner) { return sendCommand( @@ -10754,7 +10936,6 @@ private boolean sendCommand( onSendFailure, failOnSendError, mirrorToSecondEngine, - releaseReason, settlement, rejectForExclusiveWinner, null); @@ -10766,7 +10947,6 @@ private boolean sendCommand( CommandSendFailureHandler onSendFailure, boolean failOnSendError, boolean mirrorToSecondEngine, - TrackingReleaseReason releaseReason, QueuedCommandSettlement settlement, boolean rejectForExclusiveWinner, ReaderStreamBinding readBoardGmaResponseBinding) { @@ -10870,7 +11050,6 @@ private boolean sendCommand( onSendFailure, failOnSendError || foregroundRestoreCommandSession.get() != null, settlement, - releaseReason, rejectForExclusiveWinner, readBoardGmaResponseBinding, mirroredEngine, @@ -10881,7 +11060,6 @@ private boolean sendCommand( onSendFailure, failOnSendError || foregroundRestoreCommandSession.get() != null, settlement, - releaseReason, rejectForExclusiveWinner, true, false, @@ -10960,7 +11138,6 @@ private boolean enqueueOrdinaryCommandWithMirror( CommandSendFailureHandler onSendFailure, boolean failOnSendError, QueuedCommandSettlement settlement, - TrackingReleaseReason releaseReason, boolean rejectForExclusiveWinner, ReaderStreamBinding readBoardGmaResponseBinding, Leelaz mirroredEngine, @@ -10982,15 +11159,13 @@ private boolean enqueueOrdinaryCommandWithMirror( () -> { if (!canAdmitOrdinaryCommandLocked( command, - releaseReason, - rejectForExclusiveWinner, + rejectForExclusiveWinner, readBoardGmaResponseBinding, null, bootstrapReceipt, startupTransactionAtAdmission) || !mirroredEngine.canAdmitOrdinaryCommandLocked( mirroredCommand, - TrackingReleaseReason.ORDINARY_OPERATION, true, null, null, @@ -11005,8 +11180,7 @@ private boolean enqueueOrdinaryCommandWithMirror( onSendFailure, failOnSendError, settlement, - releaseReason, - true, + true, false, readBoardGmaResponseBinding, null, @@ -11019,7 +11193,6 @@ private boolean enqueueOrdinaryCommandWithMirror( null, mirroredEngine.foregroundRestoreCommandSession.get() != null, null, - TrackingReleaseReason.ORDINARY_OPERATION, true, false, null, @@ -11054,7 +11227,6 @@ private boolean enqueueOrdinaryCommand( CommandSendFailureHandler onSendFailure, boolean failOnSendError, QueuedCommandSettlement settlement, - TrackingReleaseReason releaseReason, boolean rejectForExclusiveWinner, boolean countCommand, boolean noLeelaz2Coalescing, @@ -11069,7 +11241,7 @@ private boolean enqueueOrdinaryCommand( engineGameStartupCommandContext.get(); if (Thread.holdsLock(currentQueue) && exclusiveGtpSession == null - && trackingHandoffGate == null + && settlement == null && readBoardGmaResponseBinding == null && expectedLeela0110StateToken == null) { @@ -11113,7 +11285,6 @@ && shouldCoalesceQueuedCommand(targetQueue.peekLast().command, noLeelaz2Coalesci synchronized (commandQueue()) { if (!canAdmitOrdinaryCommandLocked( command, - releaseReason, rejectForExclusiveWinner, readBoardGmaResponseBinding, expectedLeela0110StateToken, @@ -11127,7 +11298,6 @@ && shouldCoalesceQueuedCommand(targetQueue.peekLast().command, noLeelaz2Coalesci onSendFailure, failOnSendError, settlement, - releaseReason, countCommand, noLeelaz2Coalescing, readBoardGmaResponseBinding, @@ -11142,56 +11312,48 @@ && shouldCoalesceQueuedCommand(targetQueue.peekLast().command, noLeelaz2Coalesci private final class OrdinaryEnqueueEffects { private final List coalescedCommands = new ArrayList<>(); - private final List trackingNotifications = new ArrayList<>(); - private ExclusiveGtpSession trackingSession; - private int releaseStopCommandId; } + /** Requires endpoint arbitration followed by the command-queue monitor. */ private boolean canAdmitOrdinaryCommandLocked( String command, - TrackingReleaseReason releaseReason, boolean rejectForExclusiveWinner, ReaderStreamBinding readBoardGmaResponseBinding, Object expectedLeela0110StateToken, RestartBootstrapReceipt bootstrapReceipt, EngineManager.EngineGameOwnerTransaction startupTransactionAtAdmission) { - if (!hasGtpCapability() - || shouldDropStaleForegroundRestoreCommand() - || shouldSuppressNormalCommandForForegroundAnalysis() - || shouldDropCommandDuringInitialBoardSynchronizationAtAdmission(command) - || shouldRejectCommandDuringLifecycleCompletion(command) - || (startupTransactionAtAdmission != null - && !EngineManager.isEngineGameOutputAdmissionOpen(startupTransactionAtAdmission)) - || (exclusiveGtpLifecycleQueueGate - && !isExactSnapshotRestoreAdmissionContextActive() - && !isCurrentRestartBootstrapReceiptLocked(bootstrapReceipt)) - || (readBoardGmaResponseBinding != null - && (readerStreamBinding != readBoardGmaResponseBinding - || readBoardGmaResponseBinding.terminated)) - || (expectedLeela0110StateToken != null - && !isCurrentLeela0110PonderState( + return hasGtpCapability() + && !shouldDropStaleForegroundRestoreCommand() + && !shouldSuppressNormalCommandForForegroundAnalysis() + && !shouldDropCommandDuringInitialBoardSynchronizationAtAdmission(command) + && !shouldRejectCommandDuringLifecycleCompletion(command) + && (startupTransactionAtAdmission == null + || EngineManager.isEngineGameOutputAdmissionOpen(startupTransactionAtAdmission)) + && (!exclusiveGtpLifecycleQueueGate + || (lifecycleCompletionCommandContext.get() != null + && lifecycleCompletionCommandContext.get() == lifecycleCompletionClaim) + || isExactSnapshotRestoreAdmissionContextActive() + || isCurrentRestartBootstrapReceiptLocked(bootstrapReceipt)) + && (readBoardGmaResponseBinding == null + || (readerStreamBinding == readBoardGmaResponseBinding + && !readBoardGmaResponseBinding.terminated)) + && (expectedLeela0110StateToken == null + || isCurrentLeela0110PonderState( readBoardGmaResponseBinding, startupTransactionAtAdmission, expectedLeela0110StateToken)) - || (rejectForExclusiveWinner - && !isExactSnapshotRestoreAdmissionContextActive() - && (engineStateUnrestored - || readBoardGmaReservation != null - || trackingHandoffGate != null - || foregroundRestoreInProgress - || (exclusiveGtpLifecycleTransition - && exclusiveGtpLifecycleOwner != Thread.currentThread()) - || (exclusiveGtpSession != null - && !isTrackingStreamSession(exclusiveGtpSession))))) { - return false; - } - ExclusiveGtpSession trackingSession = exclusiveGtpSession; - return !isTrackingStreamSession(trackingSession) - || releaseReason != TrackingReleaseReason.SAFE_READ_ONLY_QUERY - || isSafeRawGtpQuery(command); + && (!rejectForExclusiveWinner + || isExactSnapshotRestoreAdmissionContextActive() + || (!engineStateUnrestored + && readBoardGmaReservation == null + && !foregroundRestoreInProgress + && (!exclusiveGtpLifecycleTransition + || exclusiveGtpLifecycleOwner == Thread.currentThread()) + && exclusiveGtpSession == null)); } + /** Requires endpoint arbitration followed by the command-queue monitor and prior admission. */ private QueuedCommand enqueueAdmittedOrdinaryCommandLocked( String command, @@ -11199,7 +11361,6 @@ private QueuedCommand enqueueAdmittedOrdinaryCommandLocked( CommandSendFailureHandler onSendFailure, boolean failOnSendError, QueuedCommandSettlement settlement, - TrackingReleaseReason releaseReason, boolean countCommand, boolean noLeelaz2Coalescing, ReaderStreamBinding readBoardGmaResponseBinding, @@ -11236,29 +11397,10 @@ && shouldCoalesceQueuedCommand(targetQueue.peekLast().command, noLeelaz2Coalesci expectedLeela0110StateToken), targetQueue); targetQueue.addLast(queuedCommand); - ExclusiveGtpSession trackingSession = exclusiveGtpSession; - if (isTrackingStreamSession(trackingSession) && trackingHandoffGate == null) { - TrackingReleaseDisposition disposition = - releaseReason == TrackingReleaseReason.SAFE_READ_ONLY_QUERY - ? TrackingReleaseDisposition.FROZEN_BY_SAFE - : TrackingReleaseDisposition.CLEARED; - TrackingDispositionNotification notification = - advanceTrackingReleaseDispositionLocked(trackingSession, disposition, releaseReason); - if (notification != null) { - effects.trackingNotifications.add(notification); - } - if (!trackingSession.releaseRequested) { - trackingSession.releaseRequested = true; - if (trackingSession.active) { - effects.trackingSession = trackingSession; - effects.releaseStopCommandId = claimTrackingReleaseStopLocked(trackingSession); - } - } - } return queuedCommand; } - /** Publishes callbacks and tracking transport work only after both endpoint locks are released. */ + /** Publishes coalesced-command callbacks only after both endpoint locks are released. */ private void publishOrdinaryEnqueueEffects(OrdinaryEnqueueEffects effects) { for (QueuedCommand coalesced : effects.coalescedCommands) { RuntimeException failure = @@ -11271,12 +11413,6 @@ private void publishOrdinaryEnqueueEffects(OrdinaryEnqueueEffects effects) { } } } - for (TrackingDispositionNotification notification : effects.trackingNotifications) { - notifyTrackingDisposition(notification); - } - if (effects.releaseStopCommandId != 0) { - sendTrackingReleaseStop(effects.trackingSession, effects.releaseStopCommandId); - } } private static boolean isOrdinaryPositionAnalysisCommand(String command) { @@ -11369,13 +11505,6 @@ private boolean shouldCoalesceQueuedCommand(String command, boolean noLeelaz2Coa || command.startsWith("heatmap"))); } - private static boolean isTrackingStreamSession(ExclusiveGtpSession session) { - return session != null - && session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && session.owner instanceof TrackingStreamLease - && !session.closing - && !session.closedCallbackRun; - } private Leelaz resolveDefaultCommandMirrorEngine() { if (Lizzie.config == null || !Lizzie.config.isDoubleEngineMode()) { @@ -11714,7 +11843,6 @@ private boolean sendExactSnapshotRestoreCommandAdmitted( onSendFailure, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, null, true, expectedReadBoardGmaResponseBinding(admission)); @@ -11922,7 +12050,6 @@ private void sendCommandNoLeelaz2( null, foregroundRestoreCommandSession.get() != null, startupPermit, - TrackingReleaseReason.ORDINARY_OPERATION, false, false, true, @@ -12042,7 +12169,6 @@ private void trySendCommandFromQueueNow() { QueuedCommand queuedCommand; synchronized (commandQueue()) { if (exclusiveGtpSession != null - || trackingHandoffGate != null || readerStreamRebindInProgress || normalCommandSendInProgress) { return; @@ -12054,6 +12180,15 @@ private void trySendCommandFromQueueNow() { } QueuedCommand queueHead = targetQueue.peekFirst(); ReaderStreamBinding binding = currentReaderStreamBinding(); + MoveFocusResponse focusBoundary = moveFocusBoundary; + if (focusBoundary != null && focusBoundary.binding == binding) { + if (focusBoundary.failed) return; + queueHead = null; + for (QueuedCommand candidate : targetQueue) { + if (candidate.onResponse == focusBoundary) { queueHead = candidate; break; } + } + if (queueHead == null) return; + } if (binding.queuedAnalysisStateLineage != null && binding.queuedAnalysisStateLineage.pendingRestoreOwners.get() > 0 && queueHead.engineGameTransaction() == null @@ -12078,6 +12213,8 @@ && isOrdinaryPositionAnalysisCommand(queueHead.command)) { return; } if (exclusiveGtpLifecycleQueueGate + && queueHead.completionOwner != lifecycleCompletionClaim + && queueHead.restoreAdmission == null && !isCurrentRestartBootstrapReceiptLocked(queueHead.restartBootstrapReceipt)) { return; } @@ -12356,6 +12493,10 @@ && isOrdinaryPositionAnalysisCommand(command) } currentOutputStream.write((commandLine + "\n").getBytes()); currentOutputStream.flush(); + if (isOrdinaryPositionAnalysisCommand(command) + && !(queuedCommand.onResponse instanceof MoveFocusResponse)) { + ordinaryFocusBaseCommand = command; + } if (startsAnalysisInfoPayload) { // The binding fence keeps even an immediate reader line from committing before // the new physical command invalidates the prior payload generation. @@ -12520,6 +12661,12 @@ && isOrdinaryPositionAnalysisCommand(command) private boolean claimRestartBootstrapReceiptForOutputWrite( QueuedCommand queuedCommand, BufferedOutputStream currentOutputStream) { + if (queuedCommand.restoreAdmission != null) { + return isExactSnapshotRestoreAdmissionValid(queuedCommand.restoreAdmission); + } + if (queuedCommand.completionOwner != null) { + return queuedCommand.completionOwner == lifecycleCompletionClaim; + } RestartBootstrapReceipt receipt = queuedCommand.restartBootstrapReceipt; if (receipt == null) { return true; @@ -12556,7 +12703,9 @@ private void finishRejectedCommandBeforeOutputWrite( runEngineCleanupStep( cleanupFailure, () -> queuedCommand.notifySendFailure(settledFailure)); runEngineCleanupStep(cleanupFailure, () -> noteCommandFailed(pendingHandler)); - if (queuedCommand.failOnSendError) { + if (queuedCommand.failOnSendError + && !(queuedCommand.onResponse instanceof MoveFocusResponse + && ((MoveFocusResponse) queuedCommand.onResponse).superseded)) { throw failure; } } @@ -12932,7 +13081,7 @@ private boolean requiresMatchingResponseCommandId( Runnable handler, boolean exactLoadSgf, QueuedCommand queuedCommand) { - return handler instanceof BoardSynchronizationResponseHandler + return handler instanceof MoveFocusResponse || handler instanceof BoardSynchronizationResponseHandler || (queuedCommand != null && queuedCommand.positionResponseTimeout != null) || handler instanceof EngineGameResponseHandler || handler instanceof EngineGameTimeLeftResponseHandler @@ -12964,7 +13113,7 @@ private int nextResponseCommandId( if (queuedCommand != null && queuedCommand.positionResponseTimeout != null) { return boardSynchronizationResponseCommandIds.getAndIncrement(); } - if (handler instanceof BoardSynchronizationResponseHandler) { + if (handler instanceof MoveFocusResponse || handler instanceof BoardSynchronizationResponseHandler) { return boardSynchronizationResponseCommandIds.getAndIncrement(); } if (handler instanceof EngineGameResponseHandler) { @@ -13697,33 +13846,17 @@ private ExclusiveGtpLeaseAvailability beginExclusiveGtpSession( return startReservedExclusiveGtpSession(session); } - private ExclusiveGtpSession reserveExclusiveGtpSession( - Object owner, Consumer lineConsumer, Runnable onReady, Runnable onClosed) { - return reserveExclusiveGtpSession( - owner, - lineConsumer, - onReady, - onClosed, - ExclusiveGtpReleasePolicy.FOREGROUND_RESTORE, - null); - } + private ExclusiveGtpSession reserveExclusiveGtpSession( - Object owner, - Consumer lineConsumer, - Runnable onReady, - Runnable onClosed, - ExclusiveGtpReleasePolicy releasePolicy, - ReaderStreamBinding readerBinding) { + Object owner, Consumer lineConsumer, Runnable onReady, Runnable onClosed) { ExclusiveGtpSession session = new ExclusiveGtpSession( owner, lineConsumer, onReady, onClosed, - exclusiveGtpResponseCommandIds.getAndIncrement(), - releasePolicy, - readerBinding); + exclusiveGtpResponseCommandIds.getAndIncrement()); session.wasPondering = isPondering(); exclusiveGtpSession = session; return session; @@ -13732,25 +13865,7 @@ private ExclusiveGtpSession reserveExclusiveGtpSession( private ExclusiveGtpLeaseAvailability startReservedExclusiveGtpSession( ExclusiveGtpSession session) { notPondering(); - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession != session - || session.trackingInitialWriteState != TrackingWriteState.UNSENT) { - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - session.trackingInitialWriteState = TrackingWriteState.WRITING; - } - } scheduleExclusiveGtpInitialStopTimeout(session); - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - ExclusiveGtpWriteResult writeResult = - writeExclusiveGtpCommandResult( - session, - ExclusiveGtpWritePhase.INITIAL_STOP, - session.stopCommandId, - session.stopCommandId + " stop"); - return publishTrackingInitialWriteResult(session, writeResult); - } if (!writeExclusiveGtpCommand( session, ExclusiveGtpWritePhase.INITIAL_STOP, @@ -13768,56 +13883,7 @@ private ExclusiveGtpLeaseAvailability startReservedExclusiveGtpSession( return ExclusiveGtpLeaseAvailability.AVAILABLE; } - private ExclusiveGtpLeaseAvailability publishTrackingInitialWriteResult( - ExclusiveGtpSession session, ExclusiveGtpWriteResult writeResult) { - boolean closeStaleSession = false; - boolean failCurrentSession = false; - boolean completeEarlyBoundary = false; - String earlyErrorResponse = null; - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session - && session.trackingInitialWriteState == TrackingWriteState.WRITING) { - if (readerStreamBinding != session.readerBinding || session.readerBinding.terminated) { - closeStaleSession = true; - } else if (writeResult == ExclusiveGtpWriteResult.SENT) { - session.trackingInitialWriteState = TrackingWriteState.SENT; - earlyErrorResponse = session.initialStopErrorResponse; - completeEarlyBoundary = - session.initialStopAcknowledged && session.initialStopTerminated; - } else { - session.trackingInitialWriteState = TrackingWriteState.FAILED; - failCurrentSession = true; - } - } - } - if (closeStaleSession) { - closeStaleTrackingStreamLease(session, false); - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (failCurrentSession) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.INITIAL_STOP_SEND_FAILED, - "failed to send initial stop command", - true); - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (earlyErrorResponse != null) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.INITIAL_STOP_ERROR_RESPONSE, - "initial stop command failed: " + earlyErrorResponse, - true); - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (completeEarlyBoundary) { - completeExclusiveGtpInitialStopBoundary(session); - } - return writeResult == ExclusiveGtpWriteResult.SENT - && session.trackingInitialWriteState == TrackingWriteState.SENT - ? ExclusiveGtpLeaseAvailability.AVAILABLE - : ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } + public ExclusiveGtpLeaseAvailability beginForegroundAnalysisLease( Object owner, Consumer lineConsumer, Runnable onReady, Runnable onClosed) { @@ -13883,126 +13949,14 @@ public ForegroundAnalysisLeaseAcquisition acquireForegroundAnalysisLease( lease); } - public TrackingStreamLeaseAcquisition acquireTrackingStreamLease( - Consumer lineConsumer, - Consumer onReady, - Consumer onClosed) { - return acquireTrackingStreamLease(lineConsumer, onReady, onClosed, null); - } - public TrackingStreamLeaseAcquisition acquireTrackingStreamLease( - Consumer lineConsumer, - Consumer onReady, - Consumer onClosed, - TrackingReleaseDispositionObserver dispositionObserver) { - ExclusiveGtpSession session; - TrackingStreamLease lease; - synchronized (engineArbitrationLock()) { - synchronized (commandQueue()) { - ExclusiveGtpLeaseAvailability availability = trackingStreamLeaseAvailability(); - if (availability != ExclusiveGtpLeaseAvailability.AVAILABLE - || normalCommandSendInProgress - || !commandQueue().isEmpty() - || !foregroundRestoreCommandQueue().isEmpty() - || lineConsumer == null) { - return new TrackingStreamLeaseAcquisition( - availability == ExclusiveGtpLeaseAvailability.AVAILABLE - ? ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY - : availability, - null, - null, - null); - } - ReaderStreamBinding binding = currentReaderStreamBinding(); - TrackingStreamLeaseReceipt receipt = - new TrackingStreamLeaseReceipt(this, binding.incarnation, isPondering()); - TrackingStreamLease reservedLease = - new TrackingStreamLease(this, receipt, dispositionObserver); - lease = reservedLease; - session = - reserveExclusiveGtpSession( - reservedLease, - lineConsumer, - () -> { - if (onReady != null) { - onReady.accept(reservedLease); - } - }, - () -> { - if (onClosed != null) { - onClosed.accept(reservedLease); - } - }, - ExclusiveGtpReleasePolicy.STREAM_ONLY, - binding); - } - } - ExclusiveGtpLeaseAvailability availability = startReservedExclusiveGtpSession(session); - return new TrackingStreamLeaseAcquisition( - availability, - availability == ExclusiveGtpLeaseAvailability.AVAILABLE ? lease : null, - availability == ExclusiveGtpLeaseAvailability.AVAILABLE ? lease.receipt() : null, - lease); - } - private ExclusiveGtpLeaseAvailability trackingStreamLeaseAvailability() { - if (Lizzie.leelaz == null) { - return ExclusiveGtpLeaseAvailability.NO_FOREGROUND_ENGINE; - } - if (Lizzie.leelaz != this) { - return ExclusiveGtpLeaseAvailability.NOT_CURRENT_FOREGROUND_ENGINE; - } - if (isWebTrialEngineBusy()) { - return ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE; - } - ExclusiveGtpLeaseAvailability staticAvailability = trackingStaticAvailability(); - if (staticAvailability != ExclusiveGtpLeaseAvailability.AVAILABLE) { - return staticAvailability; - } - if (engineStateUnrestored) { - return ExclusiveGtpLeaseAvailability.ENGINE_STATE_UNRESTORED; - } - if (readBoardGmaReservation != null) { - return ExclusiveGtpLeaseAvailability.READBOARD_GMA; - } - if (exclusiveGtpLifecycleTransition || hasLifecycleCompletionLocked()) { - return ExclusiveGtpLeaseAvailability.ENGINE_LIFECYCLE; - } - if (trackingHandoffGate != null) { - return ExclusiveGtpLeaseAvailability.EXISTING_LEASE; - } - if (!isLoaded() || !isStarted()) { - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (exclusiveGtpSession != null) { - return ExclusiveGtpLeaseAvailability.EXISTING_LEASE; - } - return foregroundEngineUseAvailability(); - } private boolean isWebTrialEngineBusy() { return Lizzie.webBoardManager != null && Lizzie.webBoardManager.isEngineOperationExcludedByTrial(); } - private ExclusiveGtpLeaseAvailability trackingStaticAvailability() { - if (useRemoteCompute || useJavaSSH || isSSH) { - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (Lizzie.config != null && Lizzie.config.isDoubleEngineMode()) { - return ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE; - } - if (!isKatago) { - return ExclusiveGtpLeaseAvailability.NOT_KATAGO; - } - if (outputStream == null || !endGetCommandList) { - return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; - } - if (!commandLists.contains("stop") || !commandLists.contains("kata-analyze")) { - return ExclusiveGtpLeaseAvailability.MISSING_CAPABILITY; - } - return ExclusiveGtpLeaseAvailability.AVAILABLE; - } private void recordForegroundAnalysisLeaseFailure( ExclusiveGtpSession session, ForegroundAnalysisLeaseFailure failure) { @@ -14011,12 +13965,6 @@ private void recordForegroundAnalysisLeaseFailure( } } - private void recordTrackingStreamLeaseFailure( - ExclusiveGtpSession session, TrackingStreamLeaseFailure failure) { - if (session != null && session.owner instanceof TrackingStreamLease) { - ((TrackingStreamLease) session.owner).recordFailure(failure); - } - } public ExclusiveGtpLeaseAvailability previewForegroundAnalysisLeaseAvailability() { synchronized (engineArbitrationLock()) { @@ -14029,11 +13977,6 @@ public ExclusiveGtpLeaseAvailability previewForegroundAnalysisLeaseAvailability( if (isWebTrialEngineBusy()) { return ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE; } - synchronized (commandQueue()) { - if (canClaimTrackingHandoffLocked()) { - return ExclusiveGtpLeaseAvailability.AVAILABLE; - } - } ExclusiveGtpLeaseAvailability intrinsic = intrinsicExclusiveGtpLeaseAvailability(); if (intrinsic != ExclusiveGtpLeaseAvailability.AVAILABLE) { return intrinsic; @@ -14042,20 +13985,6 @@ public ExclusiveGtpLeaseAvailability previewForegroundAnalysisLeaseAvailability( } } - private boolean canClaimTrackingHandoffLocked() { - ExclusiveGtpSession session = exclusiveGtpSession; - return trackingHandoffGate == null - && session != null - && session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && session.owner instanceof TrackingStreamLease - && !session.closing - && !session.releaseRequested - && !exclusiveGtpLifecycleTransition - && !hasLifecycleCompletionLocked() - && !normalCommandSendInProgress - && commandQueue().isEmpty() - && foregroundRestoreCommandQueue().isEmpty(); - } private ExclusiveGtpLeaseAvailability foregroundEngineUseAvailability() { if (Lizzie.frame != null @@ -14102,9 +14031,6 @@ private ExclusiveGtpLeaseAvailability intrinsicExclusiveGtpLeaseAvailability() { if (exclusiveGtpLifecycleTransition || hasLifecycleCompletionLocked()) { return ExclusiveGtpLeaseAvailability.ENGINE_LIFECYCLE; } - if (trackingHandoffGate != null) { - return ExclusiveGtpLeaseAvailability.EXISTING_LEASE; - } if (!isLoaded() || !isStarted() || outputStream == null) { return ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY; } @@ -14144,126 +14070,8 @@ public boolean sendExclusiveGtpCommand(String command) { session, ExclusiveGtpWritePhase.ACTIVE_COMMAND, 0, command); } - private boolean sendTrackingStreamCommand(TrackingStreamLease owner, String command) { - ExclusiveGtpSession session; - int commandId; - synchronized (engineArbitrationLock()) { - session = exclusiveGtpSession; - if (session == null - || session.owner != owner - || session.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || !session.active - || session.releaseRequested - || session.trackingActiveWriteState != TrackingWriteState.UNSENT - || command == null - || command.trim().isEmpty()) { - return false; - } - session.trackingActiveWriteState = TrackingWriteState.WRITING; - commandId = exclusiveGtpResponseCommandIds.getAndIncrement(); - } - ExclusiveGtpWriteResult writeResult = - writeExclusiveGtpCommandResult( - session, ExclusiveGtpWritePhase.ACTIVE_COMMAND, 0, commandId + " " + command.trim()); - int releaseStopCommandId = 0; - boolean failCurrentSession = false; - boolean closeStaleSession = false; - boolean sentForCurrentSession = false; - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session - && session.trackingActiveWriteState == TrackingWriteState.WRITING) { - if (readerStreamBinding != session.readerBinding || session.readerBinding.terminated) { - closeStaleSession = true; - } else if (writeResult == ExclusiveGtpWriteResult.SENT) { - session.trackingActiveWriteState = TrackingWriteState.SENT; - sentForCurrentSession = true; - if (session.releaseRequested) { - releaseStopCommandId = claimTrackingReleaseStopLocked(session); - } - } else { - session.trackingActiveWriteState = TrackingWriteState.FAILED; - failCurrentSession = true; - } - } - } - if (closeStaleSession) { - closeStaleTrackingStreamLease(session, true); - } else if (failCurrentSession) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.ACTIVE_COMMAND_SEND_FAILED, - "failed to send active tracking command", - true); - } else if (writeResult != ExclusiveGtpWriteResult.SENT) { - if (!isCurrentTrackingStreamIncarnation(session)) { - closeStaleTrackingStreamLease(session, true); - } - } else if (releaseStopCommandId != 0) { - sendTrackingReleaseStop(session, releaseStopCommandId); - } - return sentForCurrentSession; - } - private int claimTrackingReleaseStopLocked(ExclusiveGtpSession session) { - if (!session.active - || session.releaseStopCommandId != 0 - || session.trackingActiveWriteState == TrackingWriteState.WRITING - || session.trackingActiveWriteState == TrackingWriteState.FAILED) { - return 0; - } - int commandId = exclusiveGtpResponseCommandIds.getAndIncrement(); - session.releaseStopCommandId = commandId; - session.trackingFinalWriteState = TrackingWriteState.WRITING; - return commandId; - } - private void sendTrackingReleaseStop( - ExclusiveGtpSession session, int releaseStopCommandId) { - scheduleExclusiveGtpReleaseStopTimeout(session); - ExclusiveGtpWriteResult writeResult = - writeExclusiveGtpCommandResult( - session, - ExclusiveGtpWritePhase.RELEASE_STOP, - releaseStopCommandId, - releaseStopCommandId + " stop"); - boolean closeStaleSession = false; - boolean failCurrentSession = false; - boolean completeEarlyBoundary = false; - String earlyErrorResponse = null; - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session - && session.trackingFinalWriteState == TrackingWriteState.WRITING) { - if (readerStreamBinding != session.readerBinding || session.readerBinding.terminated) { - closeStaleSession = true; - } else if (writeResult == ExclusiveGtpWriteResult.SENT) { - session.trackingFinalWriteState = TrackingWriteState.SENT; - earlyErrorResponse = session.releaseStopErrorResponse; - completeEarlyBoundary = - session.releaseStopAcknowledged && session.releaseStopTerminated; - } else { - session.trackingFinalWriteState = TrackingWriteState.FAILED; - failCurrentSession = true; - } - } - } - if (closeStaleSession) { - closeStaleTrackingStreamLease(session, true); - } else if (failCurrentSession) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.FINAL_STOP_SEND_FAILED, - "failed to send final stop command", - true); - } else if (earlyErrorResponse != null) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.FINAL_STOP_ERROR_RESPONSE, - "final stop command failed: " + earlyErrorResponse, - true); - } else if (completeEarlyBoundary) { - completeTrackingReleaseBoundary(session); - } - } public void endExclusiveGtpSession() { ExclusiveGtpSession session; @@ -14305,7 +14113,6 @@ public boolean hasExclusiveGtpLease() { public boolean hasExclusiveGtpWorkInProgress() { synchronized (engineArbitrationLock()) { return exclusiveGtpSession != null - || trackingHandoffGate != null || foregroundRestoreInProgress || exclusiveGtpLifecycleTransition || hasLifecycleCompletionLocked(); @@ -14321,7 +14128,6 @@ boolean hasExclusiveGtpLifecycleTransitionForTest() { boolean holdUnfinishedForegroundRestoreOccupancyForTest() { synchronized (engineArbitrationLock()) { if (exclusiveGtpSession != null - || trackingHandoffGate != null || hasLifecycleCompletionLocked() || (exclusiveGtpLifecycleTransition && exclusiveGtpLifecycleOwner != null @@ -14397,11 +14203,10 @@ boolean canArmReadBoardGma() { && (Lizzie.config == null || !Lizzie.config.isDoubleEngineMode()) && readBoardGmaReservation == null && !hasLifecycleCompletionLocked() - && trackingHandoffGate == null + && !foregroundRestoreInProgress && !exclusiveGtpLifecycleTransition - && (exclusiveGtpSession == null - || exclusiveGtpSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY); + && exclusiveGtpSession == null; } } @@ -14418,51 +14223,15 @@ ExclusiveGtpLifecycleReservation beginExclusiveGtpLifecycleReservation(Object ow private ExclusiveGtpLifecycleReservation beginExclusiveGtpLifecycleReservationInternal( Object owner) { - ExclusiveGtpSession trackingSession = null; - TrackingDispositionNotification dispositionNotification = null; - int releaseStopCommandId = 0; - boolean trackingFirstWinner = false; synchronized (engineArbitrationLock()) { - synchronized (commandQueue()) { - if (isWebTrialEngineBusy()) { - return null; - } - if (exclusiveGtpSession == null) { - if (!beginExclusiveGtpLifecycleTransition(owner)) { - return null; - } - } else { - trackingSession = exclusiveGtpSession; - if (!(trackingSession.owner instanceof TrackingStreamLease) - || trackingSession.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || trackingSession.closing - || trackingSession.releaseRequested - || trackingHandoffGate != null - || exclusiveGtpLifecycleTransition) { - return null; - } - exclusiveGtpLifecycleTransition = true; - exclusiveGtpLifecycleQueueGate = true; - exclusiveGtpLifecycleOwner = owner; - exclusiveGtpLifecycleDepth = 1; - trackingSession.releaseRequested = true; - trackingFirstWinner = true; - dispositionNotification = - advanceTrackingReleaseDispositionLocked( - trackingSession, TrackingReleaseDisposition.CLEARED); - if (trackingSession.active) { - releaseStopCommandId = claimTrackingReleaseStopLocked(trackingSession); - } - } + if (isWebTrialEngineBusy() || !beginExclusiveGtpLifecycleTransition(owner)) { + return null; } } - notifyTrackingDisposition(dispositionNotification); - if (releaseStopCommandId != 0) { - sendTrackingReleaseStop(trackingSession, releaseStopCommandId); - } - return new ExclusiveGtpLifecycleReservation(this, owner, trackingFirstWinner); + return new ExclusiveGtpLifecycleReservation(this, owner); } + public EngineModeReservation beginEngineModeReservation() { synchronized (engineArbitrationLock()) { if (isWebTrialEngineBusy() || engineStateUnrestored || readBoardGmaReservation != null) { @@ -14477,7 +14246,7 @@ public EngineModeReservation beginEngineModeReservation() { } private boolean beginExclusiveGtpLifecycleTransition(Object owner) { - if (exclusiveGtpSession != null || trackingHandoffGate != null) { + if (exclusiveGtpSession != null) { return false; } if (hasLifecycleCompletionOwnedByOtherLocked(owner)) { @@ -14500,7 +14269,6 @@ private boolean beginExclusiveGtpLifecycleTransition(Object owner) { private boolean isHandoffableForegroundRestoreOccupancyLocked() { if (!exclusiveGtpLifecycleTransition || exclusiveGtpSession != null - || trackingHandoffGate != null || hasLifecycleCompletionLocked()) { return false; } @@ -14621,7 +14389,6 @@ protected void displayExclusiveGtpConflictMessage(String key) { private boolean hasConflictingExclusiveGtpWork() { synchronized (engineArbitrationLock()) { if (exclusiveGtpSession != null - || trackingHandoffGate != null || foregroundRestoreInProgress || hasLifecycleCompletionLocked()) { return true; @@ -14776,21 +14543,19 @@ private boolean hasConflictingExactSnapshotRestoreWorkLocked() { private boolean hasConflictingExactSnapshotRestoreWorkLocked(Object allowedLifecycleOwner) { return (engineStateUnrestored && !allowsUnrestoredLifecycleOwner(allowedLifecycleOwner)) || readBoardGmaReservation != null - || trackingHandoffGate != null || foregroundRestoreInProgress || hasLifecycleCompletionOwnedByOtherLocked(allowedLifecycleOwner) || (exclusiveGtpLifecycleTransition && (allowedLifecycleOwner == null || exclusiveGtpLifecycleOwner != allowedLifecycleOwner)) - || (exclusiveGtpSession != null && !isTrackingStreamSession(exclusiveGtpSession)); + || exclusiveGtpSession != null; } private boolean hasConflictingBoardSyncRestoreWorkLocked() { return readBoardGmaReservation != null || readBoardGmaRestoreBarrier != null - || trackingHandoffGate != null || foregroundRestoreInProgress || exclusiveGtpLifecycleTransition - || (exclusiveGtpSession != null && !isTrackingStreamSession(exclusiveGtpSession)); + || exclusiveGtpSession != null; } private static boolean allowsUnrestoredLifecycleOwner(Object owner) { @@ -15054,124 +14819,10 @@ private boolean failForegroundRestoreAdmission( return true; } - public TrackingHandoffClaim claimTrackingHandoff(TrackingHandoffTarget target) { - if (target == null) { - return TrackingHandoffClaim.rejected( - this, target, TrackingHandoffAvailability.INVALID_TARGET); - } - TrackingHandoffKind kind; - try { - kind = target.kind(); - } catch (Throwable ignored) { - kind = null; - } - if (kind == null) { - return TrackingHandoffClaim.rejected( - this, target, TrackingHandoffAvailability.INVALID_TARGET); - } - ExclusiveGtpSession session; - TrackingHandoffClaim claim; - TrackingDispositionNotification dispositionNotification; - int releaseStopCommandId = 0; - synchronized (engineArbitrationLock()) { - synchronized (commandQueue()) { - session = exclusiveGtpSession; - if (trackingHandoffGate != null) { - return TrackingHandoffClaim.rejected(this, target, TrackingHandoffAvailability.BUSY); - } - if (session == null || session.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY) { - return TrackingHandoffClaim.rejected( - this, target, TrackingHandoffAvailability.NOT_TRACKING); - } - if (!(session.owner instanceof TrackingStreamLease) - || session.closing - || session.releaseRequested - || exclusiveGtpLifecycleTransition - || normalCommandSendInProgress - || !commandQueue().isEmpty() - || !foregroundRestoreCommandQueue().isEmpty()) { - return TrackingHandoffClaim.rejected(this, target, TrackingHandoffAvailability.BUSY); - } - claim = new TrackingHandoffClaim(this, target, kind, session.wasPondering); - trackingHandoffGate = claim; - session.trackingHandoffClaim = claim; - session.releaseRequested = true; - dispositionNotification = - advanceTrackingReleaseDispositionLocked(session, TrackingReleaseDisposition.CLEARED); - if (session.active) { - releaseStopCommandId = claimTrackingReleaseStopLocked(session); - } - } - } - notifyTrackingDisposition(dispositionNotification); - if (releaseStopCommandId != 0) { - sendTrackingReleaseStop(session, releaseStopCommandId); - } - return claim; - } - private TrackingDispositionNotification advanceTrackingReleaseDispositionLocked( - ExclusiveGtpSession session, TrackingReleaseDisposition disposition) { - return advanceTrackingReleaseDispositionLocked(session, disposition, null); - } - private TrackingDispositionNotification advanceTrackingReleaseDispositionLocked( - ExclusiveGtpSession session, - TrackingReleaseDisposition disposition, - TrackingReleaseReason reason) { - if (session == null - || exclusiveGtpSession != session - || session.closedCallbackRun - || !(session.owner instanceof TrackingStreamLease)) { - return null; - } - TrackingStreamLease lease = (TrackingStreamLease) session.owner; - return lease.advanceDisposition(disposition) - ? new TrackingDispositionNotification(lease.dispositionObserver, disposition, reason) - : null; - } - private void notifyTrackingDisposition(TrackingDispositionNotification notification) { - if (notification == null || notification.observer == null) { - return; - } - if (notification.reason != null) { - try { - notification.observer.onReleaseClaimed(notification.reason); - } catch (Throwable ignored) { - // Observer failures do not own transport settlement. - } - } - try { - notification.observer.onDispositionChanged(notification.disposition); - } catch (Throwable ignored) { - // Observer failures do not own transport settlement. - } - } - private boolean endTrackingStreamLease(TrackingStreamLease owner) { - ExclusiveGtpSession session; - int releaseStopCommandId = 0; - synchronized (engineArbitrationLock()) { - session = exclusiveGtpSession; - if (session == null - || session.owner != owner - || session.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || session.closing - || session.releaseRequested) { - return false; - } - session.releaseRequested = true; - if (session.active) { - releaseStopCommandId = claimTrackingReleaseStopLocked(session); - } - } - if (releaseStopCommandId == 0) { - return true; - } - sendTrackingReleaseStop(session, releaseStopCommandId); - return true; - } protected long foregroundReleaseStopTimeoutMillis() { return FOREGROUND_RELEASE_STOP_TIMEOUT_MILLIS; @@ -15224,11 +14875,6 @@ private void cancelExclusiveGtpInitialStopTimeout(ExclusiveGtpSession session) { } private void failExclusiveGtpInitialStop(ExclusiveGtpSession session, String detail) { - if (session != null && session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - failTrackingStreamLease( - session, TrackingStreamLeaseFailure.INITIAL_STOP_TIMEOUT, detail, true); - return; - } if (!abortExclusiveGtpSession( session, true, ForegroundAnalysisLeaseFailure.INITIAL_STOP_TIMEOUT)) { return; @@ -15238,6 +14884,7 @@ private void failExclusiveGtpInitialStop(ExclusiveGtpSession session, String det System.err.println(message); } + private void scheduleExclusiveGtpReleaseStopTimeout(ExclusiveGtpSession session) { Timer timeout = new Timer("lizzie-exclusive-gtp-release-stop-timeout", true); TimerTask timeoutTask = @@ -15245,20 +14892,11 @@ private void scheduleExclusiveGtpReleaseStopTimeout(ExclusiveGtpSession session) @Override public void run() { executeForegroundReleaseStopTimeout( - () -> { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.FINAL_STOP_TIMEOUT, - "final stop response timeout", - true); - } else { + () -> failForegroundLeaseRelease( session, ForegroundAnalysisLeaseFailure.FINAL_STOP_TIMEOUT, - "final stop response timeout"); - } - }); + "final stop response timeout")); } }; long timeoutMillis = foregroundReleaseStopTimeoutMillis(); @@ -15272,6 +14910,7 @@ public void run() { } } + private void cancelExclusiveGtpReleaseStopTimeout(ExclusiveGtpSession session) { Timer timeout; synchronized (engineArbitrationLock()) { @@ -15308,91 +14947,9 @@ private void failForegroundLeaseRelease( restoreAfterClosedForegroundLease(session); } - private void failTrackingStreamLease( - ExclusiveGtpSession session, - TrackingStreamLeaseFailure failure, - String detail, - boolean notifyClosed) { - TrackingStreamCleanup cleanup = - claimTrackingStreamCleanup(session, failure, detail, false, true); - if (cleanup == null) { - return; - } - cancelExclusiveGtpInitialStopTimeout(session); - cancelExclusiveGtpReleaseStopTimeout(session); - String message = "Tracking stream lease failed: " + detail; - rememberRecentLine(recentStderrLines, message); - System.err.println(message); - try { - notifyTrackingDisposition(cleanup.dispositionNotification); - notifyGtpCommandStateReset(cleanup.commandStateReset); - } finally { - if (notifyClosed && isCurrentTrackingStreamIncarnation(session)) { - terminateReaderIncarnation(session.readerBinding, null); - } else { - closeStreamOnlyExclusiveGtpSession(session, false, notifyClosed); - } - } - } - private TrackingStreamCleanup claimTrackingStreamCleanup( - ExclusiveGtpSession expectedSession, - TrackingStreamLeaseFailure failure, - String detail, - boolean retiringForRebind, - boolean invalidateTransport) { - synchronized (engineArbitrationLock()) { - synchronized (commandQueue()) { - if (expectedSession == null - || exclusiveGtpSession != expectedSession - || expectedSession.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || readerStreamBinding != expectedSession.readerBinding - || expectedSession.closing) { - return null; - } - if (retiringForRebind) { - readerStreamRebindInProgress = true; - retireAnalysisOutputBindingLocked(expectedSession.readerBinding); - } - recordTrackingStreamLeaseFailure(expectedSession, failure); - expectedSession.releaseStopFailed = true; - expectedSession.closing = true; - if (invalidateTransport) { - isLoaded = false; - outputStream = null; - } - GtpCommandStateReset commandStateReset = - retiringForRebind - ? resetGtpCommandStateForReaderRebindLocked(detail) - : resetGtpCommandStateLocked(detail); - TrackingDispositionNotification dispositionNotification = - advanceTrackingReleaseDispositionLocked( - expectedSession, TrackingReleaseDisposition.CLEARED); - return new TrackingStreamCleanup( - expectedSession, commandStateReset, dispositionNotification); - } - } - } - private boolean isCurrentTrackingStreamIncarnation(ExclusiveGtpSession session) { - synchronized (engineArbitrationLock()) { - return session != null - && readerStreamBinding == session.readerBinding - && !session.readerBinding.terminated; - } - } - private void closeStaleTrackingStreamLease( - ExclusiveGtpSession session, boolean notifyClosed) { - synchronized (engineArbitrationLock()) { - if (session == null || exclusiveGtpSession != session || session.closedCallbackRun) { - return; - } - recordTrackingStreamLeaseFailure(session, TrackingStreamLeaseFailure.TRANSPORT_CLOSED); - session.closing = true; - } - closeStreamOnlyExclusiveGtpSession(session, false, notifyClosed); - } private void restoreAfterClosedForegroundLease(ExclusiveGtpSession session) { cancelExclusiveGtpInitialStopTimeout(session); @@ -15954,12 +15511,7 @@ private ExclusiveGtpWriteResult writeExclusiveGtpCommandResult( } return ExclusiveGtpWriteResult.SENT; } catch (IOException ex) { - boolean partialWrite = clearBufferedCommandBytesAfterSendFailure(currentOutputStream); - if (partialWrite - && expectedSession != null - && expectedSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - invalidateCommandOutputStreamAfterPartialWrite(currentOutputStream, command); - } + clearBufferedCommandBytesAfterSendFailure(currentOutputStream); rememberRecentLine( recentStderrLines, "Failed to send exclusive remote GTP command '" + command + "': " + ex.getMessage()); @@ -15977,35 +15529,24 @@ private boolean canWriteExclusiveGtpCommand( || expectedSession.restoreCompleted) { return false; } - if (expectedSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && (readerStreamBinding != expectedSession.readerBinding - || expectedSession.readerBinding.terminated)) { - return false; - } switch (phase) { case INITIAL_STOP: return !expectedSession.active && !expectedSession.releaseRequested - && expectedSession.stopCommandId == expectedCommandId - && (expectedSession.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || expectedSession.trackingInitialWriteState == TrackingWriteState.WRITING); + && expectedSession.stopCommandId == expectedCommandId; case ACTIVE_COMMAND: - return expectedSession.active - && (expectedSession.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - ? expectedSession.trackingActiveWriteState == TrackingWriteState.WRITING - : !expectedSession.releaseRequested); + return expectedSession.active && !expectedSession.releaseRequested; case RELEASE_STOP: return expectedSession.active && expectedSession.releaseRequested && !expectedSession.releaseStopFailed - && expectedSession.releaseStopCommandId == expectedCommandId - && (expectedSession.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || expectedSession.trackingFinalWriteState == TrackingWriteState.WRITING); + && expectedSession.releaseStopCommandId == expectedCommandId; default: return false; } } + private boolean dispatchExclusiveGtpLine(String line) { return dispatchExclusiveGtpLine(currentReaderStreamBinding(), line); } @@ -16015,37 +15556,14 @@ private boolean dispatchExclusiveGtpLine(ReaderStreamBinding binding, String lin if (session == null) { return false; } - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && session.readerBinding != binding) { - closeStaleTrackingStreamLease(session, true); - return false; - } String trimmed = line == null ? "" : line.trim(); if (!session.active) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.FOREGROUND_RESTORE - && trimmed.startsWith("info ")) { + if (trimmed.startsWith("info ")) { return true; } if (trimmed.startsWith("?") && parseResponseCommandId(trimmed) == session.stopCommandId) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - boolean failNow = false; - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session && !session.closing) { - session.initialStopErrorResponse = trimmed; - failNow = session.trackingInitialWriteState == TrackingWriteState.SENT; - } - } - if (failNow) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.INITIAL_STOP_ERROR_RESPONSE, - "initial stop command failed: " + trimmed, - true); - } - } else { - abortExclusiveGtpSession( - session, true, ForegroundAnalysisLeaseFailure.INITIAL_STOP_ERROR_RESPONSE); - } + abortExclusiveGtpSession( + session, true, ForegroundAnalysisLeaseFailure.INITIAL_STOP_ERROR_RESPONSE); return true; } if (trimmed.isEmpty() && completeExclusiveGtpInitialStopBoundary(session)) { @@ -16054,40 +15572,9 @@ private boolean dispatchExclusiveGtpLine(ReaderStreamBinding binding, String lin return false; } if (session.releaseRequested) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && session.releaseStopCommandId == 0) { - recordTrackingAnalyzeTerminator(session, trimmed); - session.lineConsumer.accept(line == null ? "" : line); - return true; - } - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && session.trackingActiveWriteState == TrackingWriteState.SENT - && !session.trackingAnalyzeClosed) { - recordTrackingAnalyzeTerminator(session, trimmed); - return true; - } int responseCommandId = parseResponseCommandId(trimmed); if (responseCommandId == session.releaseStopCommandId) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - boolean failNow = false; - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session && !session.closing) { - if (trimmed.startsWith("?")) { - session.releaseStopErrorResponse = trimmed; - failNow = session.trackingFinalWriteState == TrackingWriteState.SENT; - } else if (trimmed.startsWith("=")) { - session.releaseStopAcknowledged = true; - } - } - } - if (failNow) { - failTrackingStreamLease( - session, - TrackingStreamLeaseFailure.FINAL_STOP_ERROR_RESPONSE, - "final stop command failed: " + trimmed, - true); - } - } else if (trimmed.startsWith("?")) { + if (trimmed.startsWith("?")) { failForegroundLeaseRelease( session, ForegroundAnalysisLeaseFailure.FINAL_STOP_ERROR_RESPONSE, @@ -16101,371 +15588,86 @@ private boolean dispatchExclusiveGtpLine(ReaderStreamBinding binding, String lin } return true; } - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY - && trimmed.isEmpty() - && session.releaseStopAcknowledged) { - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session && !session.closing) { - session.releaseStopTerminated = true; - } - } - completeTrackingReleaseBoundary(session); - return true; - } boolean restore = false; synchronized (engineArbitrationLock()) { if (exclusiveGtpSession == session && !session.closing && session.releaseStopAcknowledged && trimmed.isEmpty()) { - session.closing = true; - restore = true; - } - } - if (restore) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - closeStreamOnlyExclusiveGtpSession(session); - } else { - beforeForegroundReleaseRestoreAfterBoundary(); - restoreAfterClosedForegroundLease(session); - } - } - return true; - } - recordTrackingAnalyzeTerminator(session, trimmed); - session.lineConsumer.accept(line == null ? "" : line); - return true; - } - - private boolean completeTrackingReleaseBoundary(ExclusiveGtpSession session) { - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession != session - || session.closing - || session.trackingFinalWriteState != TrackingWriteState.SENT - || !session.releaseStopAcknowledged - || !session.releaseStopTerminated - || (session.trackingActiveWriteState == TrackingWriteState.SENT - && !session.trackingAnalyzeClosed)) { - return false; - } - session.closing = true; - } - closeStreamOnlyExclusiveGtpSession(session); - return true; - } - - private void recordTrackingAnalyzeTerminator(ExclusiveGtpSession session, String line) { - if (session.releasePolicy != ExclusiveGtpReleasePolicy.STREAM_ONLY - || !line.isEmpty() - || (session.trackingActiveWriteState != TrackingWriteState.WRITING - && session.trackingActiveWriteState != TrackingWriteState.SENT)) { - return; - } - synchronized (engineArbitrationLock()) { - if (exclusiveGtpSession == session && !session.closing) { - session.trackingAnalyzeClosed = true; - } - } - } - - private void acknowledgeExclusiveGtpInitialStop(String line) { - synchronized (engineArbitrationLock()) { - ExclusiveGtpSession session = exclusiveGtpSession; - if (session == null - || session.active - || session.closing - || line == null - || !line.trim().startsWith("=") - || parseResponseCommandId(line) != session.stopCommandId) { - return; - } - session.initialStopAcknowledged = true; - } - } - - private boolean completeExclusiveGtpInitialStopBoundary(ExclusiveGtpSession session) { - Runnable onReady = null; - boolean restore = false; - synchronized (engineArbitrationLock()) { - if (session == null - || exclusiveGtpSession != session - || session.active - || session.closing - || !session.initialStopAcknowledged) { - return false; - } - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - session.initialStopTerminated = true; - if (session.trackingInitialWriteState != TrackingWriteState.SENT) { - return true; - } - } - session.active = true; - if (session.releaseRequested) { - session.closing = true; - restore = true; - } else { - onReady = session.onReady; - } - } - cancelExclusiveGtpInitialStopTimeout(session); - if (restore) { - if (session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY) { - closeStreamOnlyExclusiveGtpSession(session); - } else { - restoreAfterClosedForegroundLease(session); - } - } - if (onReady != null) { - onReady.run(); - } - return true; - } - - private void closeStreamOnlyExclusiveGtpSession(ExclusiveGtpSession session) { - closeStreamOnlyExclusiveGtpSession(session, true, true); - } - - private void closeStreamOnlyExclusiveGtpSession( - ExclusiveGtpSession session, boolean advanceOrdinaryQueue, boolean notifyClosed) { - cancelExclusiveGtpInitialStopTimeout(session); - cancelExclusiveGtpReleaseStopTimeout(session); - TrackingHandoffClaim handoff = session == null ? null : session.trackingHandoffClaim; - boolean promoteHandoff = - handoff != null - && handoff.state.get() == TrackingHandoffState.ACCEPTED_PENDING - && session.trackingLeaseFailureReason() == null; - if (!closeExclusiveGtpSession(session, promoteHandoff ? false : advanceOrdinaryQueue)) { - return; - } - runStreamOnlyClosedCallback(session, notifyClosed); - if (promoteHandoff) { - promoteTrackingHandoff(handoff); - } else if (handoff != null) { - failTrackingHandoff(handoff, TrackingHandoffFailure.TRACKING_FAILED); - } - } - - private void runStreamOnlyClosedCallback(ExclusiveGtpSession session, boolean notifyClosed) { - if (!notifyClosed) { - return; - } - Runnable onClosed; - synchronized (engineArbitrationLock()) { - if (session.closedCallbackRun) { - return; - } - session.closedCallbackRun = true; - onClosed = session.onClosed; - } - runTrackingCallback(onClosed); - } - - private void runTrackingCallback(Runnable callback) { - if (callback == null) { - return; - } - try { - callback.run(); - } catch (Throwable ignored) { - // Tracking callbacks run after ownership has already settled. - } - } - - private void promoteTrackingHandoff(TrackingHandoffClaim claim) { - synchronized (engineArbitrationLock()) { - if (trackingHandoffGate != claim - || !claim.state.compareAndSet( - TrackingHandoffState.ACCEPTED_PENDING, TrackingHandoffState.ACTIVATING)) { - return; - } - } - try { - if (!claim.target.isCurrent()) { - failTrackingHandoff(claim, TrackingHandoffFailure.CONTEXT_INVALIDATED); - return; - } - synchronized (engineArbitrationLock()) { - if (trackingHandoffGate != claim - || claim.state.get() != TrackingHandoffState.ACTIVATING) { - return; - } - claim.activationCallbackInProgress = true; - } - claim.target.activate(new TrackingHandoffActivationImpl(claim)); - if (claim.state.get() == TrackingHandoffState.ACTIVATING) { - failTrackingHandoff(claim, TrackingHandoffFailure.ACTIVATION_FAILED); - } - } catch (Throwable failure) { - failTrackingHandoff(claim, TrackingHandoffFailure.ACTIVATION_FAILED); - } finally { - settleTrackingHandoffAfterActivationCallback(claim); - } - } - - private boolean completeRetainedTrackingHandoff(TrackingHandoffClaim claim) { - synchronized (engineArbitrationLock()) { - if (trackingHandoffGate != claim - || claim.kind != TrackingHandoffKind.RETAINED_ENGINE_MODE - || !claim.state.compareAndSet( - TrackingHandoffState.ACTIVATING, TrackingHandoffState.ACTIVE)) { - return false; + session.closing = true; + restore = true; + } + } + if (restore) { + beforeForegroundReleaseRestoreAfterBoundary(); + restoreAfterClosedForegroundLease(session); } - trackingHandoffGate = null; + return true; } - trySendCommandFromQueue(); + session.lineConsumer.accept(line == null ? "" : line); return true; } - private EngineModeReservation beginRetainedTrackingHandoffReservation( - TrackingHandoffClaim claim) { + + + + private void acknowledgeExclusiveGtpInitialStop(String line) { synchronized (engineArbitrationLock()) { - if (trackingHandoffGate != claim - || claim.kind != TrackingHandoffKind.RETAINED_ENGINE_MODE - || exclusiveGtpSession != null - || exclusiveGtpLifecycleTransition - || engineStateUnrestored - || readBoardGmaReservation != null - || !claim.state.compareAndSet( - TrackingHandoffState.ACTIVATING, TrackingHandoffState.ACTIVE)) { - return null; + ExclusiveGtpSession session = exclusiveGtpSession; + if (session == null + || session.active + || session.closing + || line == null + || !line.trim().startsWith("=") + || parseResponseCommandId(line) != session.stopCommandId) { + return; } - Object owner = Thread.currentThread(); - trackingHandoffGate = null; - exclusiveGtpLifecycleTransition = true; - exclusiveGtpLifecycleOwner = owner; - exclusiveGtpLifecycleDepth = 1; - return new EngineModeReservation(this, owner); + session.initialStopAcknowledged = true; } } - private boolean activateForegroundTrackingHandoff( - TrackingHandoffClaim claim, Consumer lineConsumer, Runnable onClosed) { + private boolean completeExclusiveGtpInitialStopBoundary(ExclusiveGtpSession session) { + Runnable onReady = null; + boolean restore = false; synchronized (engineArbitrationLock()) { - if (trackingHandoffGate != claim - || claim.kind != TrackingHandoffKind.FOREGROUND_ANALYSIS - || lineConsumer == null - || !claim.state.compareAndSet( - TrackingHandoffState.ACTIVATING, TrackingHandoffState.ACTIVE)) { + if (session == null + || exclusiveGtpSession != session + || session.active + || session.closing + || !session.initialStopAcknowledged) { return false; } - ExclusiveGtpSession session = - new ExclusiveGtpSession( - claim.target, - lineConsumer, - null, - onClosed, - exclusiveGtpResponseCommandIds.getAndIncrement(), - ExclusiveGtpReleasePolicy.FOREGROUND_RESTORE, - null); session.active = true; - session.wasPondering = claim.wasPondering; - exclusiveGtpSession = session; - suppressNormalCommandsForForegroundAnalysis = true; - trackingHandoffGate = null; + if (session.releaseRequested) { + session.closing = true; + restore = true; + } else { + onReady = session.onReady; + } + } + cancelExclusiveGtpInitialStopTimeout(session); + if (restore) { + restoreAfterClosedForegroundLease(session); + } + if (onReady != null) { + onReady.run(); } return true; } - private boolean failTrackingHandoff(TrackingHandoffClaim claim, TrackingHandoffFailure failure) { - TrackingHandoffFailureSettlement settlement; - synchronized (engineArbitrationLock()) { - settlement = claimTrackingHandoffFailureLocked(claim, failure); - } - notifyTrackingHandoffFailure(settlement.notification); - return settlement.won; - } - private TrackingHandoffFailureSettlement claimTrackingHandoffFailureLocked( - TrackingHandoffClaim claim, TrackingHandoffFailure failure) { - TrackingHandoffState current = claim.state.get(); - if (current == TrackingHandoffState.FAILED || current == TrackingHandoffState.ACTIVE) { - return TrackingHandoffFailureSettlement.NOT_WON; - } - if (current == TrackingHandoffState.ACTIVATING - && claim.activationCallbackInProgress - && failure == TrackingHandoffFailure.TARGET_CANCELLED) { - return TrackingHandoffFailureSettlement.NOT_WON; - } - if (!claim.state.compareAndSet(current, TrackingHandoffState.FAILED)) { - return TrackingHandoffFailureSettlement.NOT_WON; - } - if (current == TrackingHandoffState.ACTIVATING && claim.activationCallbackInProgress) { - claim.deferredFailure = failure; - return TrackingHandoffFailureSettlement.WON_DEFERRED; - } - if (trackingHandoffGate == claim) { - trackingHandoffGate = null; - } - return new TrackingHandoffFailureSettlement( - true, new TrackingHandoffFailureNotification(claim.target, failure)); - } - private void settleTrackingHandoffAfterActivationCallback(TrackingHandoffClaim claim) { - TrackingHandoffFailureNotification notification = null; - synchronized (engineArbitrationLock()) { - if (trackingHandoffGate == claim - && claim.state.get() == TrackingHandoffState.FAILED - && claim.deferredFailure != null) { - notification = - new TrackingHandoffFailureNotification(claim.target, claim.deferredFailure); - claim.deferredFailure = null; - } - } - try { - notifyTrackingHandoffFailure(notification); - } finally { - boolean gateCleared = false; - synchronized (engineArbitrationLock()) { - if (trackingHandoffGate == claim - && claim.state.get() == TrackingHandoffState.FAILED) { - trackingHandoffGate = null; - gateCleared = true; - } - claim.activationCallbackInProgress = false; - engineArbitrationLock().notifyAll(); - } - if (gateCleared) { - trySendCommandFromQueue(); - } - } - } - private void notifyTrackingHandoffFailure(TrackingHandoffFailureNotification notification) { - if (notification == null) { - return; - } - try { - notification.target.fail(notification.failure); - } catch (Throwable ignored) { - // Target failures cannot retain the queue gate. - } finally { - trySendCommandFromQueue(); - } - } - private final class TrackingHandoffActivationImpl implements TrackingHandoffActivation { - private final TrackingHandoffClaim claim; - private TrackingHandoffActivationImpl(TrackingHandoffClaim claim) { - this.claim = claim; - } - @Override - public boolean activateForegroundAnalysis(Consumer lineConsumer, Runnable onClosed) { - return activateForegroundTrackingHandoff(claim, lineConsumer, onClosed); - } - @Override - public boolean completeRetainedEngineMode() { - return completeRetainedTrackingHandoff(claim); - } - @Override - public EngineModeReservation beginRetainedEngineModeReservation() { - return beginRetainedTrackingHandoffReservation(claim); - } - } + + + + + private void abortExclusiveGtpSession() { abortExclusiveGtpSession(exclusiveGtpSession); @@ -16486,7 +15688,6 @@ private boolean abortExclusiveGtpSession( if (session == null || session != expectedSession || session.closing - || session.releasePolicy == ExclusiveGtpReleasePolicy.STREAM_ONLY || (onlyBeforeReady && session.active)) { return false; } @@ -16919,24 +16120,14 @@ private static final class ExclusiveGtpSession { private final Runnable onReady; private final Runnable onClosed; private final int stopCommandId; - private final ExclusiveGtpReleasePolicy releasePolicy; - private final ReaderStreamBinding readerBinding; private volatile boolean active; private boolean initialStopAcknowledged; private Timer initialStopTimeout; private boolean wasPondering; private volatile boolean closing; private volatile boolean releaseRequested; - private volatile TrackingWriteState trackingInitialWriteState = TrackingWriteState.UNSENT; - private String initialStopErrorResponse; - private boolean initialStopTerminated; - private volatile TrackingWriteState trackingActiveWriteState = TrackingWriteState.UNSENT; - private volatile boolean trackingAnalyzeClosed; private volatile int releaseStopCommandId; - private volatile TrackingWriteState trackingFinalWriteState = TrackingWriteState.UNSENT; private volatile boolean releaseStopAcknowledged; - private String releaseStopErrorResponse; - private boolean releaseStopTerminated; private boolean releaseStopFailed; private volatile boolean restoreCompleted; private boolean restoreFailed; @@ -16949,251 +16140,28 @@ private static final class ExclusiveGtpSession { private Thread restoreThread; private Timer releaseStopTimeout; private Timer restoreTimeout; - private boolean closedCallbackRun; - private TrackingHandoffClaim trackingHandoffClaim; - - private TrackingStreamLeaseFailure trackingLeaseFailureReason() { - if (!(owner instanceof TrackingStreamLease)) { - return null; - } - return ((TrackingStreamLease) owner).failureReason.get(); - } private ExclusiveGtpSession( Object owner, Consumer lineConsumer, Runnable onReady, Runnable onClosed, - int stopCommandId, - ExclusiveGtpReleasePolicy releasePolicy, - ReaderStreamBinding readerBinding) { + int stopCommandId) { this.owner = owner; this.lineConsumer = lineConsumer; this.onReady = onReady; this.onClosed = onClosed; this.stopCommandId = stopCommandId; - this.releasePolicy = releasePolicy; - this.readerBinding = readerBinding; - } - } - - private static final class TrackingDispositionNotification { - private final TrackingReleaseDispositionObserver observer; - private final TrackingReleaseDisposition disposition; - private final TrackingReleaseReason reason; - - private TrackingDispositionNotification( - TrackingReleaseDispositionObserver observer, - TrackingReleaseDisposition disposition, - TrackingReleaseReason reason) { - this.observer = observer; - this.disposition = disposition; - this.reason = reason; - } - } - - private static final class TrackingHandoffFailureNotification { - private final TrackingHandoffTarget target; - private final TrackingHandoffFailure failure; - - private TrackingHandoffFailureNotification( - TrackingHandoffTarget target, TrackingHandoffFailure failure) { - this.target = target; - this.failure = failure; - } - } - - private static final class TrackingHandoffFailureSettlement { - private static final TrackingHandoffFailureSettlement NOT_WON = - new TrackingHandoffFailureSettlement(false, null); - private static final TrackingHandoffFailureSettlement WON_DEFERRED = - new TrackingHandoffFailureSettlement(true, null); - - private final boolean won; - private final TrackingHandoffFailureNotification notification; - - private TrackingHandoffFailureSettlement( - boolean won, TrackingHandoffFailureNotification notification) { - this.won = won; - this.notification = notification; - } - } - - public static final class TrackingStreamLeaseReceipt { - private final Leelaz engine; - private final long engineIncarnation; - private final boolean wasPondering; - - private TrackingStreamLeaseReceipt( - Leelaz engine, long engineIncarnation, boolean wasPondering) { - this.engine = engine; - this.engineIncarnation = engineIncarnation; - this.wasPondering = wasPondering; - } - - public Leelaz engine() { - return engine; - } - - public long engineIncarnation() { - return engineIncarnation; - } - - public boolean wasPondering() { - return wasPondering; - } - } - - public static final class TrackingStreamLeaseAcquisition { - private final ExclusiveGtpLeaseAvailability availability; - private final TrackingStreamLease lease; - private final TrackingStreamLeaseReceipt receipt; - private final TrackingStreamLease failureSource; - - private TrackingStreamLeaseAcquisition( - ExclusiveGtpLeaseAvailability availability, - TrackingStreamLease lease, - TrackingStreamLeaseReceipt receipt, - TrackingStreamLease failureSource) { - this.availability = availability; - this.lease = lease; - this.receipt = receipt; - this.failureSource = failureSource; - } - - public ExclusiveGtpLeaseAvailability availability() { - return availability; - } - - public TrackingStreamLease lease() { - return lease; - } - - public TrackingStreamLeaseReceipt receipt() { - return receipt; - } - - public Optional failureReason() { - return failureSource == null ? Optional.empty() : failureSource.failureReason(); } } - public static final class TrackingStreamLease { - private final Leelaz engine; - private final TrackingStreamLeaseReceipt receipt; - private final TrackingReleaseDispositionObserver dispositionObserver; - private final AtomicReference disposition = - new AtomicReference<>(TrackingReleaseDisposition.ACTIVE); - private final AtomicReference failureReason = - new AtomicReference<>(); - - private TrackingStreamLease( - Leelaz engine, - TrackingStreamLeaseReceipt receipt, - TrackingReleaseDispositionObserver dispositionObserver) { - this.engine = engine; - this.receipt = receipt; - this.dispositionObserver = dispositionObserver; - } - - public TrackingStreamLeaseReceipt receipt() { - return receipt; - } - - public boolean isOwned() { - return engine.hasExclusiveGtpLeaseOwnedBy(this); - } - - public boolean send(String command) { - return engine.sendTrackingStreamCommand(this, command); - } - - public boolean release() { - return engine.endTrackingStreamLease(this); - } - - public Optional failureReason() { - return Optional.ofNullable(failureReason.get()); - } - - public TrackingReleaseDisposition disposition() { - return disposition.get(); - } - - private boolean advanceDisposition(TrackingReleaseDisposition next) { - TrackingReleaseDisposition current; - do { - current = disposition.get(); - if (current.ordinal() >= next.ordinal()) { - return false; - } - } while (!disposition.compareAndSet(current, next)); - return true; - } - private void recordFailure(TrackingStreamLeaseFailure failure) { - failureReason.compareAndSet(null, failure); - } - } - public static final class TrackingHandoffClaim { - private final Leelaz engine; - private final TrackingHandoffTarget target; - private final TrackingHandoffKind kind; - private final boolean wasPondering; - private final TrackingHandoffAvailability availability; - private final AtomicReference state; - private boolean activationCallbackInProgress; - private TrackingHandoffFailure deferredFailure; - - private TrackingHandoffClaim( - Leelaz engine, - TrackingHandoffTarget target, - TrackingHandoffKind kind, - boolean wasPondering) { - this.engine = engine; - this.target = target; - this.kind = kind; - this.wasPondering = wasPondering; - this.availability = TrackingHandoffAvailability.ACCEPTED_PENDING; - this.state = new AtomicReference<>(TrackingHandoffState.ACCEPTED_PENDING); - } - private TrackingHandoffClaim( - Leelaz engine, TrackingHandoffTarget target, TrackingHandoffAvailability availability) { - this.engine = engine; - this.target = target; - this.kind = null; - this.wasPondering = false; - this.availability = availability; - this.state = new AtomicReference<>(TrackingHandoffState.FAILED); - } - private static TrackingHandoffClaim rejected( - Leelaz engine, TrackingHandoffTarget target, TrackingHandoffAvailability availability) { - return new TrackingHandoffClaim(engine, target, availability); - } - public TrackingHandoffAvailability availability() { - return availability; - } - public TrackingHandoffState state() { - return state.get(); - } - public boolean cancel() { - if (availability != TrackingHandoffAvailability.ACCEPTED_PENDING) { - return false; - } - TrackingHandoffState current = state.get(); - if (current != TrackingHandoffState.ACCEPTED_PENDING - && current != TrackingHandoffState.ACTIVATING) { - return false; - } - return engine.failTrackingHandoff(this, TrackingHandoffFailure.TARGET_CANCELLED); - } - } public static final class ForegroundAnalysisLeaseAcquisition { private final ExclusiveGtpLeaseAvailability availability; @@ -18093,9 +17061,9 @@ private void performBoardSynchronizationAttempt( // Preserve the single-engine override seam; the production two-argument method delegates // to the paired implementation, while controlled/fake engines may provide their own // exact final-fence implementation here. - authority.confirmBoardSynchronization(ownedSuccess, ownedFailure); + runAsCompletionFenceOwner(() -> authority.confirmBoardSynchronization(ownedSuccess, ownedFailure)); } else { - authority.confirmBoardSynchronization(capturedMirror, ownedSuccess, ownedFailure); + runAsCompletionFenceOwner(() -> authority.confirmBoardSynchronization(capturedMirror, ownedSuccess, ownedFailure)); } } catch (RuntimeException | Error failure) { runAsCompletionFenceOwner( @@ -18282,19 +17250,12 @@ public void close() { } public static final class ExclusiveGtpLifecycleReservation extends EngineModeReservation { - private final boolean trackingFirstWinner; - - private ExclusiveGtpLifecycleReservation( - Leelaz engine, Object owner, boolean trackingFirstWinner) { + private ExclusiveGtpLifecycleReservation(Leelaz engine, Object owner) { super(engine, owner); - this.trackingFirstWinner = trackingFirstWinner; - } - - boolean isTrackingFirstWinner() { - return trackingFirstWinner; } } + private static final class TrackedLoadSgfConsumer { private final Leelaz targetEngine; private final Path sgfFile; @@ -18917,6 +17878,11 @@ private static final class QueuedCommand { private final ReaderStreamBinding readBoardGmaResponseBinding; private final Object expectedLeela0110StateToken; private final ExactSnapshotRestoreAdmission restoreAdmission = exactSnapshotRestoreAdmissionContext.get(); + private final LifecycleCompletionClaim completionOwner = + lifecycleCompletionCommandContext.get() != null + && lifecycleCompletionCommandContext.get().authority.lifecycleCompletionClaim + == lifecycleCompletionCommandContext.get() + ? lifecycleCompletionCommandContext.get() : null; private boolean foregroundRestoreCommand; private RuntimeException cancellationFailure; private boolean outputWriteStarted; @@ -18972,9 +17938,14 @@ private QueuedCommand( this.failOnSendError = failOnSendError; this.settlement = settlement; this.countedCommand = countedCommand; - this.restartBootstrapReceipt = restartBootstrapReceipt; + this.restartBootstrapReceipt = restoreAdmission != null + || lifecycleCompletionCommandContext.get() != null + ? null : restartBootstrapReceipt; this.readBoardGmaResponseBinding = readBoardGmaResponseBinding; this.expectedLeela0110StateToken = expectedLeela0110StateToken; + if (onResponse instanceof MoveFocusResponse) { + ((MoveFocusResponse) onResponse).command = this; + } if (settlement != null) { settlement.onQueued(this); } @@ -19110,6 +18081,15 @@ private boolean beginOutputWrite() { if (cancellationFailure != null) { return false; } + if (onResponse instanceof MoveFocusResponse) { + MoveFocusResponse focus = (MoveFocusResponse) onResponse; + if (!focus.validBeforeWrite()) { + cancellationFailure = new IllegalStateException("Focus request retired before output write"); + outstandingResponseRetired = true; + focus.superseded = true; + return false; + } + } if (onResponse instanceof EngineGameResponseHandler && !((EngineGameResponseHandler) onResponse).claimPhysicalWrite()) { cancellationFailure = @@ -19337,20 +18317,6 @@ private GtpCommandStateReset( } } - private static final class TrackingStreamCleanup { - private final ExclusiveGtpSession session; - private final GtpCommandStateReset commandStateReset; - private final TrackingDispositionNotification dispositionNotification; - - private TrackingStreamCleanup( - ExclusiveGtpSession session, - GtpCommandStateReset commandStateReset, - TrackingDispositionNotification dispositionNotification) { - this.session = session; - this.commandStateReset = commandStateReset; - this.dispositionNotification = dispositionNotification; - } - } private void rememberRecentLine(ArrayDeque lines, String line) { if (lines == null || line == null) { @@ -19495,20 +18461,7 @@ public void setResponseUpToDate() { // ignoreCmdNumber=cmdNumber-1; } - private void settleTrackingPonderAfterPlayResponse() { - if (currentCommandResponseError) { - return; - } - settleTrackingPonderResponseWatermark(); - } - private void settleTrackingPonderResponseWatermark() { - synchronized (commandQueue()) { - if (currentCmdNum < cmdNumber - 1) { - currentCmdNum++; - } - } - } /** * @param color color of stone to play @@ -19541,7 +18494,7 @@ public void playMove(Stone color, String move, boolean addPlayer, boolean blackT // throw new IllegalArgumentException( // "The stone color must be B or W, but was " + color.toString()); } - boolean continuePonderAfterMove = isPonderingOrWasPonderingBeforeTracking(); + boolean continuePonderAfterMove = isPondering(); boolean resumeAnalysisAfterMove = stopByLimit || continuePonderAfterMove; boolean ponderAfterMove = resumeAnalysisAfterMove @@ -19549,11 +18502,8 @@ public void playMove(Stone color, String move, boolean addPlayer, boolean blackT && (Lizzie.config.isAutoAna || ((Lizzie.config.analyzeBlack && color == Stone.WHITE) || (Lizzie.config.analyzeWhite && color == Stone.BLACK))); - boolean settleTrackingPonder = hasTrackingStreamSession() && ponderAfterMove; retireAnalysisInfoBeforeQueuedPlay(); - sendCommand( - "play " + colorString + " " + move, - settleTrackingPonder ? this::settleTrackingPonderAfterPlayResponse : null); + sendCommand("play " + colorString + " " + move); if (Lizzie.frame.isPlayingAgainstLeelaz) this.canGetSummaryInfo = true; // bestMovesPrevious = new ArrayList<>(); if (Lizzie.frame.isAnaPlayingAgainstLeelaz @@ -19674,7 +18624,6 @@ private boolean sendEngineGameCommand( null, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, permit, false, binding); @@ -19709,7 +18658,6 @@ private boolean sendEngineGameTimeLeft( null, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, permit, false, binding); @@ -19722,7 +18670,8 @@ private boolean sendEngineGameTimeLeft( } public String addKataTag() { - return (Lizzie.config.showKataGoEstimate ? " ownership true" : "") + return (supportRootInfo ? " rootInfo true" : "") + + (Lizzie.config.showKataGoEstimate ? " ownership true" : "") + (Lizzie.config.showPvVisits ? " pvVisits true" : "") + (Lizzie.config.showKataGoEstimate && supportMovesOwnership @@ -19741,8 +18690,7 @@ public synchronized boolean genmove(String color, boolean inputCommand) { && (Lizzie.frame == null || (!Lizzie.frame.isPlayingAgainstLeelaz && !Lizzie.frame.isAnaPlayingAgainstLeelaz)); - if (!(manualRequest && hasTrackingStreamSession()) - && rejectNewExclusiveWorkDuringGtpLease()) { + if (rejectNewExclusiveWorkDuringGtpLease()) { return false; } sendPlayingAgainstHumanTimeLeftBeforeGenmove(); @@ -19799,7 +18747,6 @@ public void onResponseSettled() { null, false, true, - TrackingReleaseReason.ORDINARY_OPERATION, settlement, true); } @@ -20341,50 +19288,6 @@ private ReadBoardGmaResponseBinding currentReadBoardGmaResponseBinding() { owner, owner.currentReadBoardGmaIdentity(), owner.currentReadBoardGmaGeneration()); } - void activateReadBoardGmaAfterTracking( - TrackingHandoffTarget target, - String color, - int maxTimeSeconds, - int maxVisits, - boolean ponder, - TrackingHandoffActivation activation) { - activateReadBoardGmaAfterTracking( - target, color, maxTimeSeconds, maxVisits, ponder, activation, null); - } - - void activateReadBoardGmaAfterTracking( - TrackingHandoffTarget target, - String color, - int maxTimeSeconds, - int maxVisits, - boolean ponder, - TrackingHandoffActivation activation, - ReadBoardGmaSessionAdmission sessionAdmission) { - if (target == null - || activation == null - || isThinking - || (ponder && RemoteComputeConfig.isCustomWebSocketEngineCommand(engineCommand)) - || !beginReadBoardGmaSession(target)) { - return; - } - boolean activated = false; - try { - synchronized (readBoardGmaLock()) { - if (readBoardGmaPreparation != null) { - return; - } - readBoardGmaPreparation = - new ReadBoardGmaPreparation( - color, maxTimeSeconds, maxVisits, ponder, sessionAdmission); - } - readBoardGmaPreparation.start(); - activated = activation.completeRetainedEngineMode(); - } finally { - if (!activated) { - retireReadBoardGmaSession(); - } - } - } private boolean sendReadBoardGmaCommand( String color, ReadBoardGmaSessionAdmission sessionAdmission) { @@ -20694,35 +19597,6 @@ private boolean beginReadBoardGmaSession() { } } - private boolean beginReadBoardGmaSession(TrackingHandoffTarget target) { - synchronized (engineArbitrationLock()) { - synchronized (readBoardGmaLock()) { - if (isWebTrialEngineBusy() - || engineStateUnrestored - || hasLifecycleCompletionLocked() - || readBoardGmaRestoreBarrier != null) { - return false; - } - if (readBoardGmaReservation != null) { - return false; - } - TrackingHandoffClaim claim = trackingHandoffGate; - if (claim == null - || claim.target != target - || claim.kind != TrackingHandoffKind.RETAINED_ENGINE_MODE - || claim.state.get() != TrackingHandoffState.ACTIVATING - || exclusiveGtpLifecycleTransition) { - return false; - } - Object owner = Thread.currentThread(); - exclusiveGtpLifecycleTransition = true; - exclusiveGtpLifecycleOwner = owner; - exclusiveGtpLifecycleDepth = 1; - readBoardGmaReservation = new EngineModeReservation(this, owner); - return true; - } - } - } void retireReadBoardGmaSession() { ReadBoard readBoard = Lizzie.frame == null ? null : Lizzie.frame.readBoard; @@ -21404,7 +20278,6 @@ public void run() { this::onSendFailure, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, null, false, authority.binding); @@ -21421,7 +20294,6 @@ public void run() { this::onMirrorSendFailure, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, null, false, mirror.binding); @@ -21610,7 +20482,6 @@ private void start() { this::onSendFailure, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, null, false, barrier == null ? null : barrier.readBoardGmaResponseBinding); @@ -21922,7 +20793,6 @@ private boolean genmoveForPk( }, true, false, - TrackingReleaseReason.ORDINARY_OPERATION, null, false, binding); @@ -21978,7 +20848,7 @@ public void undo() { public void undo(boolean addPlayer, boolean blackToPlay) { synchronized (this) { - boolean continuePonderAfterUndo = isPonderingOrWasPonderingBeforeTracking(); + boolean continuePonderAfterUndo = isPondering(); sendCommand("undo"); if (continuePonderAfterUndo) if (Lizzie.config.isAutoAna @@ -22058,7 +20928,16 @@ public void ponder() { } public void ponder(boolean addPlayer, boolean blackToPlay) { + if (moveFocusCapability() == MoveFocusCapability.PROBING) return; + ponderCurrentPosition(addPlayer, blackToPlay); + } + + private void ponderCurrentPosition(boolean addPlayer, boolean blackToPlay) { if (!hasGtpCapability()) return; + MoveFocusState focus = moveFocusState; + if (focus != null && focus.current() && focus.paused && moveFocusBoundary == null) { + focus.paused = false; + } if (shouldRejectCommandDuringLifecycleCompletion()) { YikeSyncDebugLog.log("Leelaz ponder deferred: lifecycle completion pending"); return; @@ -22223,12 +21102,13 @@ public void togglePonder() { } /** - * Linearizes the analysis-control pause with tracking and ExclusiveGtp ponder handback. + * Linearizes the analysis-control pause with ExclusiveGtp ponder handback. * - *

The pause state must be recorded while holding the same lock used by both restore paths; + *

The pause state must be recorded while holding the same lock used by the restore path; * otherwise a restore can pass its pause check, then send ponder after the pause latch is set. */ public void pauseForAnalysisControl(Runnable recordPause) { + moveFocusPauseGeneration++; synchronized (analysisControlPonderLock()) { if (recordPause != null) { recordPause.run(); diff --git a/src/main/java/featurecat/lizzie/analysis/LeelazEngineCommandSink.java b/src/main/java/featurecat/lizzie/analysis/LeelazEngineCommandSink.java index b844b9405..ff9f3fc01 100644 --- a/src/main/java/featurecat/lizzie/analysis/LeelazEngineCommandSink.java +++ b/src/main/java/featurecat/lizzie/analysis/LeelazEngineCommandSink.java @@ -49,7 +49,7 @@ public void resyncFromCurrentHistory(BoardHistoryNode target) { return; } - boolean resumePonder = engine.isPonderingOrWasPonderingBeforeTracking(); + boolean resumePonder = engine.isPondering(); Leelaz mirror = engine.resolveLoadSgfMirrorEngine(); Leelaz.ExactSnapshotRestoreAdmission admission = engine.captureExactSnapshotRestoreAdmission( diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index 884ca0fab..dd03b3520 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -13,10 +13,13 @@ public class MoveData { public String coordinate; public int playouts; + /** Missing edge statistics are distinct from a known zero allocation. */ + public int edgeVisits = -1; public double winrate; public List variation; // 待完成 public List pvVisits; + public List pvEdgeVisits; public double lcb; // public double oriwinrate; public double policy; @@ -33,6 +36,23 @@ public class MoveData { public boolean lastTimeUnlimited; public long lastTimeUnlimitedTime; public boolean isSymmetry = false; + /** Root allocation when supplied by KataGo, otherwise the legacy candidate count. */ + public int allocationVisits() { + return edgeVisits >= 0 ? edgeVisits : playouts; + } + + public double allocationRatio(int denominator) { + return denominator > 0 ? (double) allocationVisits() / denominator : 0.0; + } + + public static int getAllocationVisits(List moves) { + int total = 0; + for (MoveData move : moves) { + if (move.edgeVisits < 0 || !move.isSymmetry) total += move.allocationVisits(); + } + return total; + } + public List movesEstimateArray; public MoveData() {} @@ -197,9 +217,17 @@ public static MoveData fromInfoSai(String line, boolean isSayuri) } public static MoveData fromInfoKatago(String line) throws ArrayIndexOutOfBoundsException { + return fromInfoKatago(new TokenCursor(line)); + } + + static MoveData fromInfoKatago(TokenCursor tokens) { MoveData result = new MoveData(); - TokenCursor tokens = new TokenCursor(line); + String line = tokens.value; while (tokens.next()) { + if (tokens.isTopLevelBoundary()) { + tokens.rewind(); + break; + } if (tokens.matches("pv")) { parseKataGoVariation(tokens, result); break; @@ -215,6 +243,8 @@ public static MoveData fromInfoKatago(String line) throws ArrayIndexOutOfBoundsE result.coordinate = tokens.text(); } else if (tokens.matches(line, keyStart, keyEnd, "visits")) { result.playouts = tokens.fastInt(); + } else if (tokens.matches(line, keyStart, keyEnd, "edgeVisits")) { + result.edgeVisits = tokens.fastInt(); } else if (tokens.matches(line, keyStart, keyEnd, "lcb")) { result.lcb = tokens.fastDouble() * 100; } else if (tokens.matches(line, keyStart, keyEnd, "prior")) { @@ -242,11 +272,20 @@ private static void parseKataGoVariation(TokenCursor tokens, MoveData result) { double[] ownership = null; int ownershipSize = 0; while (tokens.next()) { + if (tokens.isTopLevelBoundary()) { + tokens.rewind(); + break; + } if (tokens.matches("pvVisits")) { mode = 1; result.pvVisits = new ArrayList<>(); continue; } + if (tokens.matches("pvEdgeVisits")) { + mode = 3; + result.pvEdgeVisits = new ArrayList<>(); + continue; + } if (tokens.matches("movesOwnership")) { mode = 2; ownership = new double[Math.max(1, tokens.remainingTokenCount())]; @@ -260,6 +299,10 @@ private static void parseKataGoVariation(TokenCursor tokens, MoveData result) { if (result.pvVisits.size() < result.variation.size()) { result.pvVisits.add(tokens.text()); } + } else if (mode == 3) { + if (result.pvEdgeVisits.size() < result.variation.size()) { + result.pvEdgeVisits.add(tokens.text()); + } } else { if (ownershipSize == ownership.length) { ownership = Arrays.copyOf(ownership, ownership.length * 2); @@ -277,17 +320,26 @@ private static void parseKataGoVariation(TokenCursor tokens, MoveData result) { } } - private static final class TokenCursor { - private final String value; + static final class TokenCursor { + final String value; private int position; - private int start; - private int end; + int start; + int end; - private TokenCursor(String value) { + TokenCursor(String value) { this.value = value == null ? "" : value; } - private boolean next() { + void rewind() { + position = start; + } + + boolean isTopLevelBoundary() { + return matches("info") || matches("rootInfo") + || matches("ownership") || matches("ownershipStdev"); + } + + boolean next() { int length = value.length(); while (position < length && value.charAt(position) <= ' ') { position++; @@ -303,16 +355,16 @@ private boolean next() { return true; } - private boolean matches(String expected) { + boolean matches(String expected) { return matches(value, start, end, expected); } - private boolean matches(String source, int tokenStart, int tokenEnd, String expected) { + boolean matches(String source, int tokenStart, int tokenEnd, String expected) { return tokenEnd - tokenStart == expected.length() && source.regionMatches(tokenStart, expected, 0, expected.length()); } - private String text() { + String text() { return value.substring(start, end); } @@ -334,7 +386,7 @@ private int remainingTokenCount() { return count; } - private int fastInt() { + int fastInt() { int index = start; boolean negative = false; if (index < end && (value.charAt(index) == '-' || value.charAt(index) == '+')) { @@ -359,7 +411,7 @@ private int fastInt() { return negative ? (int) -result : (int) result; } - private double fastDouble() { + double fastDouble() { int index = start; boolean negative = false; if (index < end && (value.charAt(index) == '-' || value.charAt(index) == '+')) { @@ -426,6 +478,7 @@ private double fastDouble() { public static MoveData fromInfofromfile(String line, List bestMoves) throws ArrayIndexOutOfBoundsException { MoveData result = new MoveData(); + result.order = bestMoves.size(); String[] data = line.trim().split(" "); // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed @@ -469,6 +522,8 @@ public static MoveData fromInfofromfile(String line, List bestMoves) if (key.equals("visits")) { result.playouts = Integer.parseInt(value); } + if (key.equals("order")) result.order = Integer.parseInt(value); + if (key.equals("edgeVisits")) result.edgeVisits = Integer.parseInt(value); if (key.equals("winrate")) { // support 0.16 0.15 result.winrate = Integer.parseInt(value) / 100.0; @@ -490,7 +545,6 @@ public static MoveData fromInfofromfile(String line, List bestMoves) } } } - result.order = bestMoves.size(); return result; } diff --git a/src/main/java/featurecat/lizzie/analysis/ReadBoard.java b/src/main/java/featurecat/lizzie/analysis/ReadBoard.java index f37a15e6f..2751fced0 100644 --- a/src/main/java/featurecat/lizzie/analysis/ReadBoard.java +++ b/src/main/java/featurecat/lizzie/analysis/ReadBoard.java @@ -153,7 +153,6 @@ static boolean isYikeSyncPlatformLine(String line) { private Object readBoardGmaPendingIdentity; private long readBoardGmaPendingGeneration = 0L; private long readBoardGmaFailedGeneration = -1L; - private Leelaz.TrackingHandoffClaim readBoardGmaHandoffClaim; private boolean readBoardGmaPendingLogicallyInvalid = false; private volatile boolean readBoardGmaAwaitingSyncedBoard = false; private volatile boolean readBoardGmaEngineRestorePending = false; @@ -256,6 +255,60 @@ private void dispatchStagedTerminal() { private long trackingEligibilityRevision = 0L; private BoardHistoryNode trackingEligibilityNode; private long trackingEligibilityBoardRevision = 0L; + private AcceptedTrackingEvidence acceptedTrackingEvidence; + private long trackingFrameEpoch; + private boolean invalidTrackingFrame; + private TrackingFrameProcessing trackingFrameProcessing; + private Leelaz trackingEligibilityEngine; + private long trackingEligibilityIncarnation = -1L; + + private static final class TrackingFrameProcessing { + private final long epoch; + private final Thread thread = Thread.currentThread(); + private AcceptedTrackingEvidence accepted; + + private TrackingFrameProcessing(long epoch) { + this.epoch = epoch; + } + } + + private static final class AcceptedTrackingEvidence { + private final Board board; + private final BoardHistoryList history; + private final BoardHistoryNode node; + private final Stone[] stones; + private final boolean blackToPlay; + private final int width; + private final int height; + private final String rules; + private final double komi; + + private AcceptedTrackingEvidence(Board board, BoardHistoryNode node) { + this.board = board; + this.history = board.getHistory(); + this.node = node; + this.stones = node.getData().stones.clone(); + this.blackToPlay = node.getData().blackToPlay; + this.width = Board.boardWidth; + this.height = Board.boardHeight; + this.rules = Lizzie.config.currentKataGoRules; + this.komi = history.getGameInfo().getKomi(); + } + + private boolean matches(Board current) { + return current == board + && current.getHistory() == history + && history.getCurrentHistoryNode() == node + && Board.boardWidth == width + && Board.boardHeight == height + && node.getData().blackToPlay == blackToPlay + && java.util.Arrays.equals(stones, node.getData().stones) + && java.util.Objects.equals(rules, Lizzie.config.currentKataGoRules) + && Double.doubleToLongBits(komi) + == Double.doubleToLongBits(history.getGameInfo().getKomi()); + } + } + private ReadBoardTrackingEligibilityAdapter.Reason trackingEligibilityReason = ReadBoardTrackingEligibilityAdapter.Reason.NO_ACCEPTED_FRAME; private Object trackingEligibilityObserverIdentity; @@ -669,13 +722,29 @@ public void parseLine(String line) { } } if (line.startsWith("re=")) { - if (tempcount.isEmpty()) { - invalidateTrackingEligibility(ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING); - } - String[] params = line.substring(3).split(","); - if (params.length == Board.boardWidth) { - for (int i = 0; i < params.length; i++) - tempcount.add(Integer.parseInt(params[i].substring(0, 1))); + if (tempcount.isEmpty() && !invalidTrackingFrame) { + invalidateAcceptedTrackingEvidence( + ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING); + } + String row = line.substring(3); + if (row.endsWith("\n")) row = row.substring(0, row.length() - 1); + if (row.endsWith("\r")) row = row.substring(0, row.length() - 1); + String[] params = row.split(",", -1); + boolean valid = params.length == Board.boardWidth; + for (String cell : params) { + valid &= cell.length() == 1 && cell.charAt(0) >= '0' && cell.charAt(0) <= '4'; + } + if (!valid || tempcount.size() + params.length > Board.boardWidth * Board.boardHeight) { + invalidTrackingFrame = true; + invalidateAcceptedTrackingEvidence( + ReadBoardTrackingEligibilityAdapter.Reason.NO_ACCEPTED_FRAME); + observe( + () -> + ReadBoardObservation.recordFailure( + "parse-line", + new IllegalArgumentException("Invalid ReadBoard snapshot row"))); + } else if (!invalidTrackingFrame) { + for (String cell : params) tempcount.add(cell.charAt(0) - '0'); } } if (line.startsWith("foxMoveNumber")) { @@ -766,12 +835,15 @@ public void parseLine(String line) { boolean isYikePlatform = pendingRemoteContext != null && pendingRemoteContext.platform == SyncRemoteContext.SyncPlatform.YIKE; - if (!isSyncing && !isYikePlatform) syncBoardStones(false); + if (!invalidTrackingFrame && !isSyncing && !isYikePlatform) syncBoardStones(false); clearPendingRemoteContext(); tempcount = new ArrayList(); + invalidTrackingFrame = false; publishCurrentReadBoardDiagnosticsSnapshot(); } if (line.trim().equals("clear")) { + invalidateAcceptedTrackingEvidence(ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING); + invalidTrackingFrame = false; resetActiveSyncStateForReadBoardControlLine(); clearPendingRemoteContext(); tempcount = new ArrayList(); @@ -781,7 +853,8 @@ public void parseLine(String line) { runOnEdtAndWait(() -> Lizzie.board.clear(false)); } if (line.startsWith("start")) { - invalidateTrackingEligibility(ReadBoardTrackingEligibilityAdapter.Reason.FIRST_FRAME); + invalidateAcceptedTrackingEvidence(ReadBoardTrackingEligibilityAdapter.Reason.FIRST_FRAME); + invalidTrackingFrame = false; clearPendingRemoteContext(); String[] params = line.trim().split(" "); if (params.length >= 3) { @@ -1514,27 +1587,46 @@ public void run() { } private void syncBoardStones(boolean isSecondTime) { - if (isReadBoardGmaEngineBusy() - || Lizzie.frame.isPlayingAgainstLeelaz - || Lizzie.frame.isAnaPlayingAgainstLeelaz) { - applySyncBoardStones(isSecondTime); - return; + TrackingFrameProcessing processing; + synchronized (this) { + if (trackingFrameProcessing != null || shutdownStarted) return; + processing = new TrackingFrameProcessing(trackingFrameEpoch); + trackingFrameProcessing = processing; } - collectedSyncResumeTarget = null; - syncRecoveryThread = Thread.currentThread(); - CompletableFuture confirmed; - localNavigationTracker.beginReadBoardNavigation(); try { - confirmed = - Lizzie.board.applyReadBoardSync( - () -> applySyncBoardStones(isSecondTime), () -> collectedSyncResumeTarget != null); + if (isReadBoardGmaEngineBusy() + || Lizzie.frame.isPlayingAgainstLeelaz + || Lizzie.frame.isAnaPlayingAgainstLeelaz) { + synchronized (Lizzie.board) { + applySyncBoardStones(isSecondTime); + publishAcceptedTrackingEligibility(processing); + } + return; + } + collectedSyncResumeTarget = null; + syncRecoveryThread = Thread.currentThread(); + CompletableFuture confirmed; + localNavigationTracker.beginReadBoardNavigation(); + try { + confirmed = + Lizzie.board.applyReadBoardSync( + () -> { + applySyncBoardStones(isSecondTime); + publishAcceptedTrackingEligibility(processing); + }, + () -> collectedSyncResumeTarget != null); + } finally { + syncRecoveryThread = null; + localNavigationTracker.clear(); + isSyncing = false; + } + if (collectedSyncResumeTarget != null) { + scheduleResumeAnalysisAfterSync(collectedSyncResumeTarget, confirmed); + } } finally { - syncRecoveryThread = null; - localNavigationTracker.clear(); - isSyncing = false; - } - if (collectedSyncResumeTarget != null) { - scheduleResumeAnalysisAfterSync(collectedSyncResumeTarget, confirmed); + synchronized (this) { + trackingFrameProcessing = null; + } } } @@ -2561,11 +2653,18 @@ && isTrustedFoxZeroMoveHandicapSetupTurn( foxMoveNumber)) || isMarkerlessOrdinaryFoxTurnFallback( syncStartNode, snapshotDelta, lastMoveSource, foxMoveNumber); - BoardHistoryNode acceptedNode = - Lizzie.board == null || Lizzie.board.getHistory() == null - ? null - : Lizzie.board.getHistory().getCurrentHistoryNode(); - publishAcceptedTrackingEligibility(acceptedNode); + synchronized (this) { + TrackingFrameProcessing processing = trackingFrameProcessing; + BoardHistoryNode acceptedNode = + acceptedRealMove ? Lizzie.board.getHistory().getMainEnd() : syncStartNode; + if (processing != null + && processing.thread == Thread.currentThread() + && processing.epoch == trackingFrameEpoch + && acceptedNode != null + && sameStoneLayout(buildSnapshotStones(snapshotCodes), acceptedNode.getData().stones)) { + processing.accepted = new AcceptedTrackingEvidence(Lizzie.board, acceptedNode); + } + } } private boolean isTrustedUnchangedSnapshotTurn( @@ -2891,6 +2990,8 @@ private ArrayList startStones() { } private void stopLocalMoveConfirmation() { + invalidateAcceptedTrackingEvidence(ReadBoardTrackingEligibilityAdapter.Reason.RETIRED); + invalidTrackingFrame = false; synchronized (LOCAL_MOVE_CONFIRMATION_LOCK) { Lizzie.frame.syncBoard = false; resetActiveSyncState(); @@ -3722,7 +3823,8 @@ private void observeFailedLocalMoveSnapshot( } if (failedLocalMoveSuppressionSnapshotKey.equals(snapshotKey)) { localMoveSyncDebug( - "failed-place observation sees no remote board change; releasing failed move for fresh analysis snapshot=" + "failed-place observation sees no remote board change; releasing failed move for fresh" + + " analysis snapshot=" + snapshotSummary(snapshotCodes) + " " + pendingLocalMoveState()); @@ -4619,34 +4721,15 @@ private boolean scheduleReadBoardGmaIfNeeded(String reason) { + readBoardGmaMaxVisits + " ponder=" + ponder); - ReadBoardGmaHandoffTarget handoffTarget = - new ReadBoardGmaHandoffTarget( - helperIdentity, - sessionGeneration, - color, - readBoardGmaTimeSeconds, - readBoardGmaMaxVisits, - ponder); Lizzie.leelaz.bindReadBoardGmaResponseOwner(this, helperIdentity, sessionGeneration); - Leelaz.TrackingHandoffClaim handoff = Lizzie.leelaz.claimTrackingHandoff(handoffTarget); - if (handoff.availability() == Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING) { - readBoardGmaHandoffClaim = handoff; - } Lizzie.leelaz.setReadBoardGmaSessionAdmission(this::admitReadBoardGmaSession); boolean accepted = - handoff.availability() == Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING - || (handoff.availability() == Leelaz.TrackingHandoffAvailability.NOT_TRACKING - && Lizzie.leelaz.genmoveAnalyzeForReadBoard( - color, readBoardGmaTimeSeconds, readBoardGmaMaxVisits, ponder)); - if (handoff.availability() == Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING) { - // The handoff activation delivers the session admission through the tracking callback. - Lizzie.leelaz.setReadBoardGmaSessionAdmission(null); - } + Lizzie.leelaz.genmoveAnalyzeForReadBoard( + color, readBoardGmaTimeSeconds, readBoardGmaMaxVisits, ponder); if (!accepted) { readBoardGmaPending = false; readBoardGmaPendingLogicallyInvalid = false; readBoardGmaPendingIdentity = null; - readBoardGmaHandoffClaim = null; readBoardGmaFailedGeneration = sessionGeneration; Lizzie.leelaz.clearReadBoardGmaResponseOwner(this); localMoveSyncDebug("ReadBoard GMA rejected by foreground lease reason=" + reason); @@ -4654,70 +4737,6 @@ private boolean scheduleReadBoardGmaIfNeeded(String reason) { return true; } - private final class ReadBoardGmaHandoffTarget implements Leelaz.TrackingHandoffTarget { - private final Object helperIdentity; - private final long sessionGeneration; - private final String color; - private final int maxTimeSeconds; - private final int maxVisits; - private final boolean ponder; - - private ReadBoardGmaHandoffTarget( - Object helperIdentity, - long sessionGeneration, - String color, - int maxTimeSeconds, - int maxVisits, - boolean ponder) { - this.helperIdentity = helperIdentity; - this.sessionGeneration = sessionGeneration; - this.color = color; - this.maxTimeSeconds = maxTimeSeconds; - this.maxVisits = maxVisits; - this.ponder = ponder; - } - - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return isCurrentReadBoardGmaTarget(helperIdentity, sessionGeneration); - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - synchronized (ReadBoard.this) { - Leelaz engine = Lizzie.leelaz; - if (!isCurrentReadBoardGmaTarget(helperIdentity, sessionGeneration)) { - if (engine != null) { - engine.clearReadBoardGmaResponseOwner( - ReadBoard.this, helperIdentity, sessionGeneration); - } - return; - } - if (engine == null) { - return; - } - engine.activateReadBoardGmaAfterTracking( - this, - color, - maxTimeSeconds, - maxVisits, - ponder, - activation, - ReadBoard.this::admitReadBoardGmaSession); - readBoardGmaHandoffClaim = null; - } - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failReadBoardGmaHandoff(helperIdentity, sessionGeneration, failure); - } - } private synchronized boolean isCurrentReadBoardGmaTarget( Object helperIdentity, long sessionGeneration) { @@ -4733,22 +4752,6 @@ private synchronized boolean isCurrentReadBoardGmaTarget( && !readBoardGmaEngineRestoreInProgress; } - private synchronized void failReadBoardGmaHandoff( - Object helperIdentity, long sessionGeneration, Leelaz.TrackingHandoffFailure failure) { - if (readBoardGmaPendingIdentity != helperIdentity - || readBoardGmaPendingGeneration != sessionGeneration) { - return; - } - readBoardGmaPending = false; - readBoardGmaPendingLogicallyInvalid = false; - readBoardGmaPendingIdentity = null; - readBoardGmaHandoffClaim = null; - readBoardGmaFailedGeneration = sessionGeneration; - if (Lizzie.leelaz != null) { - Lizzie.leelaz.clearReadBoardGmaResponseOwner(this, helperIdentity, sessionGeneration); - } - localMoveSyncDebug("ReadBoard GMA handoff failed reason=" + failure); - } private void showReadBoardGmaUnsupportedOnce() { if (Lizzie.leelaz != null && Lizzie.leelaz.shouldShowReadBoardGmaUnsupportedPrompt()) { @@ -5367,7 +5370,8 @@ private boolean resumeFailedLocalMoveAfterSyncIfNeeded( if (failedLocalMoveAwaitingRemoteObservation && !releaseFailedLocalMoveObservationIfTimedOut(reason)) { localMoveSyncDebug( - "failed local move recovery leaves placement guard active while analysis may continue reason=" + "failed local move recovery leaves placement guard active while analysis may continue" + + " reason=" + reason + " target=" + historyNodeSummary(targetNode) @@ -5471,7 +5475,8 @@ private boolean resumeFailedLocalMoveAfterSyncIfNeeded( } if (Lizzie.config != null && !Lizzie.config.readBoardPonder) { localMoveSyncDebug( - "failed local move observed our turn but auto-play analysis skip readBoardPonder=false reason=" + "failed local move observed our turn but auto-play analysis skip readBoardPonder=false" + + " reason=" + reason + " target=" + historyNodeSummary(targetNode) @@ -5640,7 +5645,7 @@ && releaseFailedLocalMoveObservationIfTimedOut(reason)) { + isReadBoardAnalysisEngineAvailable()); return false; } - if (Lizzie.leelaz.isPondering() && !Lizzie.leelaz.hasTrackingStreamSession()) { + if (Lizzie.leelaz.isPondering()) { if ("rebuild".equals(reason)) { localMoveSyncDebug( "resume auto-play analysis already pondering reason=" @@ -5891,9 +5896,35 @@ private boolean beginShutdown() { } @Override - public synchronized ReadBoardTrackingEligibilityAdapter.Snapshot snapshot() { + public ReadBoardTrackingEligibilityAdapter.Snapshot snapshot() { + Leelaz engine = Lizzie.leelaz; + long incarnation = engine == null ? -1L : engine.confirmedReadBoardTrackingIncarnation(); + Board board = Lizzie.board; + if (board == null) return trackingEligibilitySnapshot(engine, -1L); + synchronized (board) { + return trackingEligibilitySnapshot(engine, incarnation); + } + } + + private synchronized ReadBoardTrackingEligibilityAdapter.Snapshot trackingEligibilitySnapshot( + Leelaz engine, long incarnation) { ensureTrackingEligibilityInitialized(); ReadBoardTrackingEligibilityAdapter.Reason reason = currentTrackingEligibilityReason(); + if (reason == ReadBoardTrackingEligibilityAdapter.Reason.STABLE) { + if (engine != Lizzie.leelaz || incarnation < 0L) { + reason = ReadBoardTrackingEligibilityAdapter.Reason.ENGINE_UNRESTORED; + } else { + long revision = Lizzie.board.getContextRevision(); + if (trackingEligibilityBoardRevision != revision + || trackingEligibilityEngine != engine + || trackingEligibilityIncarnation != incarnation) { + trackingEligibilityRevision++; + trackingEligibilityBoardRevision = revision; + trackingEligibilityEngine = engine; + trackingEligibilityIncarnation = incarnation; + } + } + } return new ReadBoardTrackingEligibilityAdapter.Snapshot( trackingEligibilityIdentity, trackingEligibilityRevision, @@ -5948,7 +5979,7 @@ private ReadBoardTrackingEligibilityAdapter.Reason currentTrackingEligibilityRea if (awaitingFirstSyncFrame) { return ReadBoardTrackingEligibilityAdapter.Reason.FIRST_FRAME; } - if (isSyncing) { + if (isSyncing || trackingFrameProcessing != null) { return ReadBoardTrackingEligibilityAdapter.Reason.SYNCING; } if (isPendingLocalMoveAwaitingReadBoard()) { @@ -5963,24 +5994,38 @@ private ReadBoardTrackingEligibilityAdapter.Reason currentTrackingEligibilityRea if (!Lizzie.leelaz.isEligibleLocalKataGoForReadBoardTracking()) { return ReadBoardTrackingEligibilityAdapter.Reason.ENGINE_UNAVAILABLE; } - if (Lizzie.board == null - || Lizzie.board.getHistory() == null - || Lizzie.board.getHistory().getCurrentHistoryNode() != trackingEligibilityNode - || Lizzie.board.getContextRevision() != trackingEligibilityBoardRevision) { + if (trackingEligibilityReason != ReadBoardTrackingEligibilityAdapter.Reason.STABLE + && trackingEligibilityReason != ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH) { + return trackingEligibilityReason; + } + if (acceptedTrackingEvidence == null || !acceptedTrackingEvidence.matches(Lizzie.board)) { return ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH; } - return trackingEligibilityReason; + return ReadBoardTrackingEligibilityAdapter.Reason.STABLE; } - private void publishAcceptedTrackingEligibility(BoardHistoryNode acceptedNode) { - if (acceptedNode == null || Lizzie.board == null) { - return; - } + private void publishAcceptedTrackingEligibility(TrackingFrameProcessing processing) { Runnable listener; synchronized (this) { + AcceptedTrackingEvidence evidence = processing.accepted; + if (processing.epoch != trackingFrameEpoch + || shutdownStarted + || evidence == null + || Lizzie.frame == null + || Lizzie.frame.readBoard != this + || Lizzie.board != evidence.board + || Lizzie.board.getHistory() != evidence.history) return; ensureTrackingEligibilityInitialized(); + if (acceptedTrackingEvidence != null + && acceptedTrackingEvidence.matches(Lizzie.board) + && trackingEligibilityNode == evidence.node) { + acceptedTrackingEvidence = evidence; + trackingEligibilityReason = ReadBoardTrackingEligibilityAdapter.Reason.STABLE; + return; + } trackingEligibilityRevision++; - trackingEligibilityNode = acceptedNode; + trackingEligibilityNode = evidence.node; + acceptedTrackingEvidence = evidence; trackingEligibilityBoardRevision = Lizzie.board.getContextRevision(); trackingEligibilityReason = ReadBoardTrackingEligibilityAdapter.Reason.STABLE; listener = takeTrackingEligibilityInvalidationListener(); @@ -5988,12 +6033,31 @@ private void publishAcceptedTrackingEligibility(BoardHistoryNode acceptedNode) { runTrackingEligibilityInvalidationListener(listener); } + private void invalidateAcceptedTrackingEvidence( + ReadBoardTrackingEligibilityAdapter.Reason reason) { + if (reason == ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING) { + synchronized (this) { + trackingFrameEpoch++; + trackingEligibilityReason = reason; + } + return; + } + synchronized (this) { + trackingFrameEpoch++; + acceptedTrackingEvidence = null; + } + invalidateTrackingEligibility(reason); + } + private void invalidateTrackingEligibility(ReadBoardTrackingEligibilityAdapter.Reason reason) { Runnable listener; synchronized (this) { ensureTrackingEligibilityInitialized(); trackingEligibilityRevision++; - trackingEligibilityReason = reason; + if (reason != ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH + || trackingEligibilityReason != ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING) { + trackingEligibilityReason = reason; + } listener = takeTrackingEligibilityInvalidationListener(); } runTrackingEligibilityInvalidationListener(listener); @@ -6001,7 +6065,6 @@ private void invalidateTrackingEligibility(ReadBoardTrackingEligibilityAdapter.R private void retireTrackingEligibility() { Runnable listener; - Leelaz.TrackingHandoffClaim handoff; if (retireReadBoardGmaSession()) { readBoardGmaAutoPlayActive = false; readBoardGmaPendingLogicallyInvalid = true; @@ -6018,6 +6081,7 @@ private void retireTrackingEligibility() { trackingEligibilityRevision++; trackingEligibilityIdentity = new Object(); trackingEligibilityNode = null; + acceptedTrackingEvidence = null; trackingEligibilityBoardRevision = 0L; trackingEligibilityReason = ReadBoardTrackingEligibilityAdapter.Reason.RETIRED; retiredReadBoardGmaTerminalPending = readBoardGmaPending; @@ -6033,19 +6097,8 @@ private void retireTrackingEligibility() { readBoardGmaEngineRestoreInProgress = false; readBoardGmaDeferredRestoreNode = null; readBoardGmaSessionBinding = null; - handoff = readBoardGmaHandoffClaim; - readBoardGmaHandoffClaim = null; } runTrackingEligibilityInvalidationListener(listener); - if (handoff != null) { - if (handoff.cancel() && Lizzie.leelaz != null) { - retiredReadBoardGmaTerminalPending = false; - retiredReadBoardGmaIdentity = null; - retiredReadBoardGmaGeneration = -1L; - Lizzie.leelaz.clearReadBoardGmaResponseOwner( - this, retiredGmaIdentity, retiredGmaGeneration); - } - } if (Lizzie.leelaz != null) { Lizzie.leelaz.retireReadBoardGmaSession(); } @@ -6096,6 +6149,10 @@ public void onHistoryOverwritten() { if (shouldSuppressHistoryOverwriteInvalidation()) { return; } + synchronized (this) { + trackingFrameEpoch++; + acceptedTrackingEvidence = null; + } synchronized (LOCAL_MOVE_CONFIRMATION_LOCK) { clearPendingLocalMoveTracking(); clearFailedLocalMoveSuppression(); diff --git a/src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java b/src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java index ff2bcca48..b8022146a 100644 --- a/src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java +++ b/src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java @@ -103,7 +103,7 @@ public TrackingAnalysisController.AddResult addPoint( if (!before.matches(readBoardContext)) { return TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE; } - source.observeInvalidation(before.identity(), controller::clear); + source.observeInvalidation(before.identity(), () -> controller.contextChanged(null)); return controller.addPoint( coordinate, context, diff --git a/src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java b/src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java index c9213228a..fedfddee2 100644 --- a/src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java +++ b/src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java @@ -1,8 +1,11 @@ package featurecat.lizzie.analysis; -import java.util.ArrayDeque; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.gui.LizzieFrame; +import featurecat.lizzie.rules.BoardHistoryNode; +import featurecat.lizzie.rules.Stone; import java.util.Collections; -import java.util.Deque; +import java.util.List; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -11,7 +14,7 @@ import java.util.Timer; import java.util.TimerTask; -/** Owns single-stream tracking requests and their immutable display state. */ +/** Owns attention points and cumulative progress in the current ordinary search tree. */ public final class TrackingAnalysisController { static final long PROGRESS_TIMEOUT_MILLIS = 8000L; @@ -222,133 +225,47 @@ private boolean matches(Context other) { public static final class DisplaySnapshot { private static final DisplaySnapshot EMPTY = - new DisplaySnapshot(null, 0L, Collections.emptySet(), Collections.emptyMap(), false, false); - + new DisplaySnapshot(null, 0L, Set.of(), Set.of(), Map.of(), false); private final Context context; private final long generation; private final Set selectedPoints; - private final Map results; - private final boolean active; - private final boolean frozen; - - private DisplaySnapshot( - Context context, - long generation, - Set selectedPoints, - Map results, - boolean active, - boolean frozen) { + private final Set activePoints; + private final Map visits; + private final boolean cancellationPending; + + private DisplaySnapshot(Context context, long generation, Set selectedPoints, + Set activePoints, Map visits, boolean cancellationPending) { this.context = context; this.generation = generation; - this.selectedPoints = Collections.unmodifiableSet(new LinkedHashSet(selectedPoints)); - this.results = Collections.unmodifiableMap(new LinkedHashMap(results)); - this.active = active; - this.frozen = frozen; - } - - public Set selectedPoints() { - return selectedPoints; - } - - public Context context() { - return context; - } - - public long generation() { - return generation; - } - - public boolean active() { - return active; - } - - public Map results() { - return results; - } - - public boolean frozen() { - return frozen; - } - } - - public static final class PointResult { - private final String coordinate; - private final int visits; - private final double winrate; - private final double scoreLead; - private final boolean completed; - - private PointResult(MoveData move) { - this.coordinate = move.coordinate; - this.visits = move.playouts; - this.winrate = move.winrate; - this.scoreLead = move.scoreMean; - this.completed = false; - } - - private PointResult(PointResult source, boolean completed) { - this.coordinate = source.coordinate; - this.visits = source.visits; - this.winrate = source.winrate; - this.scoreLead = source.scoreLead; - this.completed = completed; - } - - public String coordinate() { - return coordinate; - } - - public int visits() { - return visits; - } - - public double winrate() { - return winrate; - } - - public double scoreLead() { - return scoreLead; - } - - public boolean completed() { - return completed; + this.selectedPoints = Collections.unmodifiableSet(new LinkedHashSet<>(selectedPoints)); + this.activePoints = Collections.unmodifiableSet(new LinkedHashSet<>(activePoints)); + this.visits = Collections.unmodifiableMap(new LinkedHashMap<>(visits)); + this.cancellationPending = cancellationPending; } - private PointResult asCompleted() { - return completed ? this : new PointResult(this, true); - } - } - - private static final class PointAttempt { - private final long generation; - private final String coordinate; - private Leelaz.TrackingStreamLease lease; - private Cancellable timeout; - private long timeoutToken; - private PointResult result; - private boolean cancelled; - private boolean restorePonderOnClose; - private boolean acquisitionValidated; - private boolean ready; - private boolean requestSent; - private Leelaz.TrackingReleaseDisposition disposition = - Leelaz.TrackingReleaseDisposition.ACTIVE; - - private PointAttempt(long generation, String coordinate) { - this.generation = generation; - this.coordinate = coordinate; - } + public Context context() { return context; } + public long generation() { return generation; } + public Set selectedPoints() { return selectedPoints; } + public Set activePoints() { return activePoints; } + public Map visits() { return visits; } + public int targetVisits() { return context == null ? 0 : context.parameters.targetVisits(); } + public boolean active() { return !activePoints.isEmpty() || cancellationPending; } + public boolean cancellationPending() { return cancellationPending; } } private final TimeoutScheduler timeoutScheduler; private final Runnable displayChanged; private final LinkedHashSet selectedPoints = new LinkedHashSet<>(); - private final Deque pendingPoints = new ArrayDeque<>(); - private final LinkedHashMap results = new LinkedHashMap<>(); + private final LinkedHashSet activePoints = new LinkedHashSet<>(); + private final LinkedHashMap visits = new LinkedHashMap<>(); + private final LinkedHashMap timeouts = new LinkedHashMap<>(); + private final LinkedHashMap timeoutTokens = new LinkedHashMap<>(); + private Set installedPoints = Set.of(); private Context context; private long generation; - private PointAttempt current; - private Leelaz.TrackingStreamLeaseReceipt initialReceipt; + private long updateRevision; + private long timeoutSequence; + private boolean cancellationPending; private volatile DisplaySnapshot snapshot = DisplaySnapshot.EMPTY; public TrackingAnalysisController() { @@ -372,350 +289,214 @@ public synchronized AddResult addPoint(String coordinate, Context requestedConte return addPoint(coordinate, requestedContext, null); } - synchronized AddResult addPoint( - String coordinate, Context requestedContext, java.util.function.BooleanSupplier validator) { + synchronized AddResult addPoint(String coordinate, Context requestedContext, + java.util.function.BooleanSupplier validator) { Objects.requireNonNull(requestedContext, "requestedContext"); - String normalized = normalizeCoordinate(coordinate, requestedContext); - if (normalized == null) { - return AddResult.ILLEGAL; - } - if (context != null && !context.matches(requestedContext)) { - return AddResult.CONTEXT_MISMATCH; - } - if (current != null - && (current.cancelled - || current.disposition != Leelaz.TrackingReleaseDisposition.ACTIVE)) { + String point = normalizeCoordinate(coordinate, requestedContext); + if (point == null) return AddResult.ILLEGAL; + if (context != null && !context.matches(requestedContext)) return AddResult.CONTEXT_MISMATCH; + if (selectedPoints.contains(point)) return AddResult.DUPLICATE; + if (!requestedContext.engine.canStartMoveFocus()) return AddResult.LEASE_UNAVAILABLE; + if (!isAvailablePoint(point, requestedContext)) return AddResult.ILLEGAL; + Context acceptedContext = context == null ? requestedContext : context; + List live = acceptedContext.engine.beginMoveFocus(this, acceptedContext, + () -> isAvailablePoint(point, acceptedContext) + && (validator == null || validator.getAsBoolean())); + if (live == null) return AddResult.LEASE_UNAVAILABLE; + if (context == null) { + context = acceptedContext; + generation++; + } + selectedPoints.add(point); + activePoints.add(point); + visits.put(point, 0); + observeProgress(live); + if (activePoints.contains(point)) scheduleTimeout(point); + if (!sendUpdate(false)) { + selectedPoints.remove(point); + visits.remove(point); + publishSnapshot(); return AddResult.LEASE_UNAVAILABLE; } - if (current == null && snapshot.frozen()) { - clearPointState(); - initialReceipt = null; - publishEmptySnapshot(); - } - if (selectedPoints.contains(normalized)) { - return AddResult.DUPLICATE; - } - - context = requestedContext; - selectedPoints.add(normalized); - if (current != null) { - pendingPoints.addFirst(normalized); - publishSnapshot(true, false); - return AddResult.ADDED; - } - return startPoint(normalized, validator); + return AddResult.ADDED; } public synchronized boolean removePoint(String coordinate) { - if (context == null) { - return false; - } - String normalized = normalizeCoordinate(coordinate, context); - if (normalized == null || !selectedPoints.remove(normalized)) { - return false; - } - results.remove(normalized); - if (current != null && current.coordinate.equals(normalized)) { - current.cancelled = true; - current.restorePonderOnClose = true; - cancelTimeout(current); - publishSnapshot(false, false); - current.lease.release(); - } else { - pendingPoints.remove(normalized); - publishSnapshot(current != null, false); - } - if (selectedPoints.isEmpty() && current == null) { - context = null; - } + if (context == null) return false; + String point = normalizeCoordinate(coordinate, context); + if (point == null || !selectedPoints.remove(point)) return false; + activePoints.remove(point); + visits.remove(point); + cancelTimeout(point); + // Hide attention immediately; the installed set remains evidence until its boundary closes. + publishSnapshot(); + sendUpdate(true); return true; } public synchronized void clear() { - PointAttempt attempt = current; - clearPointState(); - if (attempt == null) { - context = null; - initialReceipt = null; - publishEmptySnapshot(); - return; - } - attempt.cancelled = true; - attempt.restorePonderOnClose = true; - cancelTimeout(attempt); - publishEmptySnapshot(); - if (attempt.lease != null) { - attempt.lease.release(); - } + if (context == null) return; + selectedPoints.clear(); + activePoints.clear(); + visits.clear(); + cancelTimeouts(); + publishSnapshot(); + sendUpdate(true); } public synchronized void contextChanged(Context currentContext) { - if (context != null && (currentContext == null || !context.matches(currentContext))) { - clearState(); - } - } - - private void clearState() { - PointAttempt attempt = current; - generation++; - current = null; - if (attempt != null) { - attempt.cancelled = true; - cancelTimeout(attempt); - } - clearPointState(); - context = null; - initialReceipt = null; - publishEmptySnapshot(); - if (attempt != null && attempt.lease != null) { - attempt.lease.release(); - } - } - - private AddResult startPoint(String coordinate) { - return startPoint(coordinate, null); - } - - private AddResult startPoint( - String coordinate, java.util.function.BooleanSupplier acquisitionValidator) { - PointAttempt attempt = new PointAttempt(++generation, coordinate); - current = attempt; - publishSnapshot(true, false); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - context.engine.acquireTrackingStreamLease( - line -> handleLine(attempt.generation, line), - lease -> handleReady(attempt.generation, lease), - lease -> handleClosed(attempt.generation, lease), - new LeaseObserver(attempt.generation)); - if (current != attempt - || acquisition.availability() != Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE - || acquisition.receipt() == null - || acquisition.receipt().engine() != context.engine - || acquisition.receipt().engineIncarnation() != context.engineIncarnation) { - if (acquisition.lease() != null) { - acquisition.lease().release(); - } - if (current == attempt) { - current = null; - } - clearPointState(); - context = null; - initialReceipt = null; - publishEmptySnapshot(); - return AddResult.LEASE_UNAVAILABLE; - } - attempt.lease = acquisition.lease(); - boolean validAfterAcquisition = - acquisitionValidator == null || acquisitionValidator.getAsBoolean(); - if (!validAfterAcquisition || current != attempt) { - attempt.cancelled = true; - if (attempt.lease != null) { - attempt.lease.release(); - } - if (current == attempt) { - current = null; - clearPointState(); - context = null; - initialReceipt = null; - publishEmptySnapshot(); + if (context == null || (currentContext != null && context.matches(currentContext))) return; + Context retired = context; + clearContext(); + retired.engine.retireMoveFocus(this, retired); + } + + public DisplaySnapshot snapshot() { return snapshot; } + + synchronized void onFocusAnalysis(Context source, List moves) { + if (context != source) return; + boolean completed = observeProgress(moves); + if (completed) sendUpdate(false); + else publishSnapshot(); + } + + private boolean observeProgress(List moves) { + boolean completed = false; + for (MoveData move : moves) { + String point = move.coordinate; + if (!selectedPoints.contains(point)) continue; + int previous = visits.getOrDefault(point, 0); + visits.put(point, Math.max(previous, move.playouts)); + if (!activePoints.contains(point)) continue; + if (move.playouts >= context.parameters.targetVisits()) { + activePoints.remove(point); + cancelTimeout(point); + completed = true; + } else if (move.playouts > previous) { + scheduleTimeout(point); } - return AddResult.LEASE_UNAVAILABLE; - } - attempt.acquisitionValidated = true; - if (initialReceipt == null) { - initialReceipt = acquisition.receipt(); } - if (attempt.ready) { - sendTrackingRequest(attempt); - } - return AddResult.ADDED; + return completed; } - public DisplaySnapshot snapshot() { - return snapshot; + synchronized void onFocusUpdateConfirmed(Context source, long revision, Set points) { + if (context != source || revision != updateRevision) return; + installedPoints = Set.copyOf(points); + cancellationPending = !activePoints.containsAll(installedPoints); + publishSnapshot(); } - private synchronized void handleReady(long expectedGeneration, Leelaz.TrackingStreamLease lease) { - PointAttempt attempt = current; - if (attempt == null - || attempt.generation != expectedGeneration - || (attempt.lease != null && attempt.lease != lease)) { - lease.release(); - return; - } - attempt.lease = lease; - attempt.ready = true; - if (!attempt.acquisitionValidated) { - return; - } - sendTrackingRequest(attempt); + synchronized void onFocusStopping(Context source, long revision, boolean gainPending) { + if (context != source || revision != updateRevision) return; + activePoints.clear(); + cancelTimeouts(); + cancellationPending = gainPending; + publishSnapshot(); } - private void sendTrackingRequest(PointAttempt attempt) { - if (current != attempt || attempt.requestSent || attempt.cancelled) { - return; - } - attempt.requestSent = true; - String command = - "kata-analyze " - + context.parameters.intervalCentiseconds() - + " allow B " - + attempt.coordinate - + " 1 allow W " - + attempt.coordinate - + " 1"; - if (attempt.lease.send(command) && current == attempt) { - scheduleProgressTimeout(attempt); - } - } - - private synchronized void handleLine(long expectedGeneration, String line) { - PointAttempt attempt = current; - if (attempt == null - || attempt.generation != expectedGeneration - || attempt.cancelled - || attempt.disposition != Leelaz.TrackingReleaseDisposition.ACTIVE - || line == null - || !line.startsWith("info ")) { + synchronized void onFocusStopped(Context source, boolean retireContext) { + if (context != source) return; + if (retireContext) { + clearContext(); return; } - PointResult parsed; - try { - parsed = new PointResult(MoveData.fromInfoKatago(line.substring(5))); - } catch (RuntimeException parseFailure) { - return; - } - if (!attempt.coordinate.equalsIgnoreCase(parsed.coordinate) - || parsed.visits <= 0 - || (attempt.result != null && parsed.visits <= attempt.result.visits)) { - return; - } - attempt.result = parsed; - results.put(attempt.coordinate, parsed); - cancelTimeout(attempt); - if (parsed.visits >= context.parameters.targetVisits()) { - attempt.lease.release(); - } else { - scheduleProgressTimeout(attempt); - } - publishSnapshot(true, false); + activePoints.clear(); + installedPoints = Set.of(); + cancellationPending = false; + updateRevision++; + cancelTimeouts(); + publishSnapshot(); } - private synchronized void handleClosed( - long expectedGeneration, Leelaz.TrackingStreamLease lease) { - PointAttempt attempt = current; - if (attempt == null - || attempt.generation != expectedGeneration - || (attempt.lease != null && attempt.lease != lease)) { - return; - } - attempt.lease = lease; - cancelTimeout(attempt); - current = null; - if (attempt.disposition != Leelaz.TrackingReleaseDisposition.ACTIVE) { - if (attempt.disposition == Leelaz.TrackingReleaseDisposition.CLEARED) { - context = null; - } - return; - } - boolean completed = - !attempt.cancelled - && lease.failureReason().isEmpty() - && attempt.result != null - && attempt.result.visits >= context.parameters.targetVisits(); - boolean cleanUserCancellation = - attempt.restorePonderOnClose && lease.failureReason().isEmpty(); - if (!completed) { - selectedPoints.remove(attempt.coordinate); - results.remove(attempt.coordinate); - } else { - attempt.result = attempt.result.asCompleted(); - results.put(attempt.coordinate, attempt.result); - } - String next = pendingPoints.pollFirst(); - if (next != null) { - startPoint(next); - } else { - publishSnapshot(false, false); - Leelaz.TrackingStreamLeaseReceipt handbackReceipt = initialReceipt; - initialReceipt = null; - if ((completed || cleanUserCancellation) - && lease.disposition() == Leelaz.TrackingReleaseDisposition.ACTIVE - && handbackReceipt != null - && context.engine == handbackReceipt.engine() - && context.engineIncarnation == handbackReceipt.engineIncarnation()) { - context.engine.restorePonderAfterTracking(handbackReceipt); - } - if (selectedPoints.isEmpty()) { - context = null; - publishEmptySnapshot(); - } + private boolean sendUpdate(boolean retireUnconsumed) { + Context target = context; + if (target == null) return false; + long revision = ++updateRevision; + cancellationPending = cancellationPending || !activePoints.containsAll(installedPoints); + publishSnapshot(); + boolean accepted = target.engine.updateMoveFocus( + this, target, Set.copyOf(activePoints), revision, retireUnconsumed); + if (!accepted && context == target) { + activePoints.clear(); + cancelTimeouts(); + // A failed update is not a cancellation acknowledgement. Engine retirement settles it. + cancellationPending = cancellationPending || !installedPoints.isEmpty(); + publishSnapshot(); } + return accepted; } - private void scheduleProgressTimeout(PointAttempt attempt) { - long token = ++attempt.timeoutToken; - attempt.timeout = - timeoutScheduler.schedule( - PROGRESS_TIMEOUT_MILLIS, () -> handleTimeout(attempt.generation, token)); + private void scheduleTimeout(String point) { + cancelTimeout(point); + long token = ++timeoutSequence; + timeoutTokens.put(point, token); + Context source = context; + timeouts.put(point, timeoutScheduler.schedule(PROGRESS_TIMEOUT_MILLIS, + () -> onTimeout(source, point, token))); } - private synchronized void handleTimeout(long expectedGeneration, long expectedToken) { - PointAttempt attempt = current; - if (attempt == null - || attempt.generation != expectedGeneration - || attempt.timeoutToken != expectedToken - || attempt.disposition != Leelaz.TrackingReleaseDisposition.ACTIVE) { - return; - } - attempt.timeout = null; - attempt.timeoutToken++; - attempt.lease.release(); + private synchronized void onTimeout(Context source, String point, long token) { + if (context != source || !Objects.equals(timeoutTokens.get(point), token) + || !activePoints.remove(point)) return; + cancelTimeout(point); + sendUpdate(true); } - private synchronized void handleDisposition( - long expectedGeneration, Leelaz.TrackingReleaseDisposition disposition) { - PointAttempt attempt = current; - if (attempt == null - || attempt.generation != expectedGeneration - || attempt.disposition.ordinal() >= disposition.ordinal()) { - return; - } - attempt.disposition = disposition; - initialReceipt = null; - cancelTimeout(attempt); - if (disposition == Leelaz.TrackingReleaseDisposition.FROZEN_BY_SAFE) { - pendingPoints.clear(); - selectedPoints.retainAll(results.keySet()); - publishSnapshot(false, !results.isEmpty()); - } else { - clearPointState(); - publishSnapshot(false, false); - } + private void cancelTimeout(String point) { + timeoutTokens.remove(point); + Cancellable timeout = timeouts.remove(point); + if (timeout != null) timeout.cancel(); } - private static void cancelTimeout(PointAttempt attempt) { - attempt.timeoutToken++; - if (attempt.timeout != null) { - attempt.timeout.cancel(); - attempt.timeout = null; - } + private void cancelTimeouts() { + for (Cancellable timeout : timeouts.values()) timeout.cancel(); + timeouts.clear(); + timeoutTokens.clear(); } - private void clearPointState() { - pendingPoints.clear(); + private void clearContext() { + cancelTimeouts(); selectedPoints.clear(); - results.clear(); + activePoints.clear(); + visits.clear(); + installedPoints = Set.of(); + cancellationPending = false; + context = null; + generation++; + updateRevision++; + publishSnapshot(); } - private void publishSnapshot(boolean active, boolean frozen) { - snapshot = new DisplaySnapshot(context, generation, selectedPoints, results, active, frozen); + private void publishSnapshot() { + snapshot = context == null ? DisplaySnapshot.EMPTY + : new DisplaySnapshot(context, generation, selectedPoints, activePoints, visits, + cancellationPending); displayChanged.run(); } - private void publishEmptySnapshot() { - snapshot = DisplaySnapshot.EMPTY; - displayChanged.run(); + private static boolean isAvailablePoint(String point, Context context) { + if (!(context.displayNodeIdentity instanceof BoardHistoryNode)) return false; + BoardHistoryNode node = (BoardHistoryNode) context.displayNodeIdentity; + char column = point.charAt(0); + int x = column - 'A' - (column > 'I' ? 1 : 0); + int y = context.boardHeight - Integer.parseInt(point.substring(1)); + Stone[] stones = node.getData().stones; + int index = x * context.boardHeight + y; + if (index >= stones.length || stones[index] != Stone.EMPTY) return false; + if (Lizzie.frame != null && (Lizzie.frame.isKeepingForce || LizzieFrame.isKeepForcing)) { + String allow = LizzieFrame.allowcoords; + if (allow != null && !allow.isEmpty()) return containsCoordinate(allow, point); + String avoid = LizzieFrame.avoidcoords; + if (avoid != null && !avoid.isEmpty()) return !containsCoordinate(avoid, point); + } + return true; + } + + private static boolean containsCoordinate(String coordinates, String point) { + for (String coordinate : coordinates.split(",")) { + if (point.equalsIgnoreCase(coordinate.trim())) return true; + } + return false; } private static String normalizeCoordinate(String coordinate, Context context) { @@ -740,27 +521,6 @@ private static String normalizeCoordinate(String coordinate, Context context) { return normalized; } - private final class LeaseObserver implements Leelaz.TrackingReleaseDispositionObserver { - private final long expectedGeneration; - - private LeaseObserver(long expectedGeneration) { - this.expectedGeneration = expectedGeneration; - } - - @Override - public void onDispositionChanged(Leelaz.TrackingReleaseDisposition disposition) { - handleDisposition(expectedGeneration, disposition); - } - - @Override - public void onReleaseClaimed(Leelaz.TrackingReleaseReason reason) { - handleDisposition( - expectedGeneration, - reason == Leelaz.TrackingReleaseReason.SAFE_READ_ONLY_QUERY - ? Leelaz.TrackingReleaseDisposition.FROZEN_BY_SAFE - : Leelaz.TrackingReleaseDisposition.CLEARED); - } - } private static final class DaemonTimeoutScheduler implements TimeoutScheduler { private final Timer timer = new Timer("lizzie-tracking-analysis-progress-timeout", true); diff --git a/src/main/java/featurecat/lizzie/gui/AnalysisFrame.java b/src/main/java/featurecat/lizzie/gui/AnalysisFrame.java index c0a6466c3..6d2e66a01 100644 --- a/src/main/java/featurecat/lizzie/gui/AnalysisFrame.java +++ b/src/main/java/featurecat/lizzie/gui/AnalysisFrame.java @@ -486,27 +486,31 @@ private void paintBottomPanel(Graphics g0, int width, int height) { // TODO Auto-generated method stub int minHeight = 22; int trueHeight = height - minHeight; - int totalPlayouts = 0; + int totalPlayouts = index == 2 + ? Lizzie.board.getData().getPlayouts2() : Lizzie.board.getData().getPlayouts(); int maxPlayouts = 0; + int maxMoveVisits = 0; double stable = 0; List bestMoves = null; if (index == 1) bestMoves = Lizzie.board.getData().bestMoves; else if (index == 2) bestMoves = Lizzie.board.getData().bestMoves2; if (bestMoves == null || bestMoves.isEmpty()) return; + int totalAllocation = MoveData.getAllocationVisits(bestMoves); Graphics2D g = (Graphics2D) g0; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (MoveData move : bestMoves) { - totalPlayouts += move.playouts; - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); + if (move.playouts > maxMoveVisits) maxMoveVisits = move.playouts; } int length = bestMoves.size(); for (int i = 0; i < 11; i++) { if (i < length) stable += - (maxPlayouts - bestMoves.get(i).playouts) * (maxPlayouts - bestMoves.get(i).playouts); - else stable += maxPlayouts * maxPlayouts; + (double) (maxPlayouts - bestMoves.get(i).allocationVisits()) + * (maxPlayouts - bestMoves.get(i).allocationVisits()); + else stable += (double) maxPlayouts * maxPlayouts; } - stable = stable / 10 / totalPlayouts / totalPlayouts; + stable = totalAllocation > 0 ? stable / 10 / totalAllocation / totalAllocation : 0; stable = Math.pow(stable, 0.5) * 100; g.setColor(Color.BLACK); g.setFont(new Font(Lizzie.config.uiFontName, Font.PLAIN, 13)); @@ -515,7 +519,7 @@ private void paintBottomPanel(Graphics g0, int width, int height) { + Utils.getPlayoutsString(totalPlayouts) + " " + resourceBundle.getString("AnalysisFrame.maxVisits") - + Utils.getPlayoutsString(maxPlayouts) + + Utils.getPlayoutsString(maxMoveVisits) + " " + resourceBundle.getString("AnalysisFrame.concentration") + String.format(Locale.ENGLISH, "%.2f", stable) @@ -532,13 +536,13 @@ private void paintBottomPanel(Graphics g0, int width, int height) { g.drawString(String.valueOf(i + 1), 3, minHeight + i * 20 + 15); g.setColor(Color.DARK_GRAY); if (i < length) { - double percents = (double) bestMoves.get(i).playouts / maxPlayouts; + double percents = bestMoves.get(i).allocationRatio(maxPlayouts); g.fillRect(20, minHeight + i * 20 + 2, (int) ((width - 80) * percents), 16); g.drawString( String.format( Locale.ENGLISH, "%.2f", - (double) bestMoves.get(i).playouts * 100 / totalPlayouts) + bestMoves.get(i).allocationRatio(totalAllocation) * 100) + "%", 26 + (int) ((width - 80) * percents), minHeight + i * 20 + 15); @@ -984,10 +988,7 @@ public int compare(MoveData s1, MoveData s2) { // MoveDataSorter(data2); // ArrayList sortedMoveData = MoveDataSorter.getSortedMoveDataByPolicy(); // double maxlcb = 0; - int totalPlayouts = 0; - for (MoveData move : data2) { - totalPlayouts = totalPlayouts + move.playouts; - } + int totalPlayouts = MoveData.getAllocationVisits(data2); MoveData data = data2.get(row); switch (col) { case 0: @@ -1022,7 +1023,7 @@ else if (data.isNextMove) else return Utils.getPlayoutsString(data.playouts); case 5: return String.format( - Locale.ENGLISH, "%.1f", (double) data.playouts * 100 / totalPlayouts); + Locale.ENGLISH, "%.1f", data.allocationRatio(totalPlayouts) * 100); case 6: if (data.isNextMove && data.policy < -1000) return "--"; return String.format(Locale.ENGLISH, "%.2f", data.policy); diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 65a723488..a7bd13115 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1641,9 +1641,9 @@ private void drawBranch() { mouseOverTempNode = Lizzie.frame.getDisplayNode(); int maxPlayouts = 0; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); } - float percentPlayouts = (float) suggestedMove.get().playouts / maxPlayouts; + float percentPlayouts = (float) suggestedMove.get().allocationRatio(maxPlayouts); // (Lizzie.config.leelaversion >= 17 && Lizzie.config.showlcbcolor && // !Lizzie.leelaz.noLcb) @@ -2579,7 +2579,7 @@ private void drawLeelazSuggestions(Graphics2D g) { double minWinrate = 100.0; double maxScoreMean = -300; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); if (move.winrate > maxWinrate) maxWinrate = move.winrate; if (move.winrate < minWinrate) minWinrate = move.winrate; if (move.isKataData && Lizzie.config.showScoremeanInSuggestion) { @@ -2609,7 +2609,6 @@ private void drawLeelazSuggestions(Graphics2D g) { } continue; // This actually can happen } - if (selectedTrackingPoints.contains(move.coordinate)) continue; Optional coordsOpt = Board.asCoordinates(move.coordinate); if (!coordsOpt.isPresent()) { continue; @@ -2653,7 +2652,7 @@ private void drawLeelazSuggestions(Graphics2D g) { Lizzie.config.winrateAlwaysBlack && !Lizzie.frame.getDisplayNode().getData().blackToPlay; double fraction = 0; - float percentPlayouts = (float) move.playouts / maxPlayouts; + float percentPlayouts = (float) move.allocationRatio(maxPlayouts); int[] coords = coordsOpt.get(); int suggestionX = x + scaledMarginWidth + squareWidth * coords[0]; @@ -2671,7 +2670,8 @@ private void drawLeelazSuggestions(Graphics2D g) { Board.getIndex(coords[0], coords[1]) < hasDrawBackground.length ? hasDrawBackground[Board.getIndex(coords[0], coords[1])] : false; - if (outOfOrder && !isMouseOver && hasBackground) continue; + if (outOfOrder && !isMouseOver && hasBackground + && !selectedTrackingPoints.contains(move.coordinate)) continue; float hue; // boolean hue2; @@ -2694,7 +2694,7 @@ private void drawLeelazSuggestions(Graphics2D g) { Color hsbColor = Color.getHSBColor(hue, saturation, brightness); Color color = new Color(hsbColor.getRed(), hsbColor.getGreen(), hsbColor.getBlue(), (int) alpha); - boolean needShow = false; + boolean needShow = selectedTrackingPoints.contains(move.coordinate); if (Lizzie.frame.priorityMoveCoords.size() > 0) { for (String coord : Lizzie.frame.priorityMoveCoords) { if (coord.equals(move.coordinate)) needShow = true; @@ -2855,7 +2855,9 @@ private void drawLeelazSuggestions(Graphics2D g) { showPlayouts, showScoreLead, canShowMaxColor && hasMaxWinrate ? maxColor : normalColor, - canShowMaxColor && move.playouts == maxPlayouts ? maxColor : normalColor, + canShowMaxColor && move.allocationVisits() == maxPlayouts + ? maxColor + : normalColor, canShowMaxColor && move.scoreMean == maxScoreMean ? maxColor : normalColor); } } @@ -2867,110 +2869,26 @@ private void drawLeelazSuggestions(Graphics2D g) { } private void drawTrackingOverlay(Graphics2D g) { - if (boardIndex != 0 || isShowingBranch) return; - float alphaFactor = 5.0f; - try { - if (Lizzie.board == null) return; - TrackingAnalysisController.DisplaySnapshot trackingSnapshot = - Lizzie.frame == null ? null : Lizzie.frame.trackingDisplaySnapshot(); - boolean currentTrackingDisplay = - Lizzie.frame != null && Lizzie.frame.isTrackingDisplayCurrent(trackingSnapshot); - MoveData ordinaryBestMove = - bestMoves == null || bestMoves.isEmpty() ? null : bestMoves.get(0); - Map trackingResults = - currentTrackingDisplay - ? trackingSnapshot.results() - : java.util.Collections.emptyMap(); - if (currentTrackingDisplay) { - java.util.Collection trackedMoves = - trackingResults.values(); - if (!trackedMoves.isEmpty()) { - long maxPlayoutsTracked = 0; - for (TrackingAnalysisController.PointResult result : trackedMoves) { - if (result.visits() > maxPlayoutsTracked) maxPlayoutsTracked = result.visits(); - } - for (TrackingAnalysisController.PointResult result : trackedMoves) { - String moveCoord = result.coordinate(); - if (moveCoord == null) continue; - int movePlayouts = result.visits(); - double moveWinrate = result.winrate(); - double moveScoreMean = result.scoreLead(); - if (movePlayouts == 0) continue; - Optional coordsOpt = Board.asCoordinates(moveCoord); - if (!coordsOpt.isPresent()) continue; - int[] tCoords = coordsOpt.get(); - if (tCoords[0] < 0 - || tCoords[0] >= Board.boardWidth - || tCoords[1] < 0 - || tCoords[1] >= Board.boardHeight) continue; - int suggestionX = x + scaledMarginWidth + squareWidth * tCoords[0]; - int suggestionY = y + scaledMarginHeight + squareHeight * tCoords[1]; - - float alphaRatio = - maxPlayoutsTracked > 0 - ? max( - 0, - (float) log((double) movePlayouts / maxPlayoutsTracked) / alphaFactor + 1) - : 1.0f; - Color interiorColor = - withOpacity( - Lizzie.config.trackingPointInteriorColor, - Lizzie.config.trackingPointInteriorOpacityPercent); - - g.setColor(interiorColor); - fillCircle(g, suggestionX, suggestionY, stoneRadius + 1); - float alphaCircle = 48 + 48 * alphaRatio; - g.setColor(new Color(0, 0, 0, (int) alphaCircle)); - drawCircle(g, suggestionX, suggestionY, stoneRadius + 1, 26.5f); - - boolean flipWinrate = - Lizzie.config.winrateAlwaysBlack - && !Lizzie.frame.getDisplayNode().getData().blackToPlay; - double roundedWinrate = round(moveWinrate * 10) / 10.0; - if (flipWinrate) roundedWinrate = 100.0 - roundedWinrate; - Color textColor = - Lizzie.config.trackingPointTextAutoColor - ? readableTextColor( - trackingInteriorDisplayColor(interiorColor, suggestionX, suggestionY)) - : Lizzie.config.trackingPointTextColor; - double ordinaryBestScore = - ordinaryBestMove == null ? moveScoreMean : ordinaryBestMove.scoreMean; - drawSuggestionText( - g, - suggestionX, - suggestionY, - roundedWinrate, - movePlayouts, - moveScoreMean, - ordinaryBestScore, - Lizzie.frame.shouldShowSuggestionWinrateFor(Lizzie.frame.getDisplayNode()), - Lizzie.frame.shouldShowSuggestionPlayoutsFor(Lizzie.frame.getDisplayNode()), - Lizzie.config.showScoremeanInSuggestion, - textColor, - textColor, - textColor); - } - } + if (boardIndex != 0 || isShowingBranch || Lizzie.frame == null) return; + TrackingAnalysisController.DisplaySnapshot snapshot = Lizzie.frame.trackingDisplaySnapshot(); + if (!Lizzie.frame.isTrackingDisplayCurrent(snapshot)) return; + BoardData data = Lizzie.frame.getDisplayNode().getData(); + MoveData baseline = null; + for (MoveData candidate : data.bestMoves) { + if (candidate.order == 0) { + baseline = candidate; + break; } - drawTrackedPointMarkers( - g, - currentTrackingDisplay - ? trackingSnapshot.selectedPoints() - : java.util.Collections.emptySet(), - trackingResults, - ordinaryBestMove); - } catch (Exception e) { - e.printStackTrace(); } + drawTrackedPointMarkers(g, snapshot, data.bestMoves, baseline); } - private Color trackingResultColor( - MoveData ordinaryBestMove, TrackingAnalysisController.PointResult result) { - if (ordinaryBestMove == null) return new Color(112, 118, 124); - double winrateLoss = ordinaryBestMove.winrate - result.winrate(); + private Color trackingResultColor(MoveData ordinaryBestMove, MoveData result) { + if (ordinaryBestMove == null || result == null) return new Color(112, 118, 124); + double winrateLoss = ordinaryBestMove.winrate - result.winrate; Optional scoreLoss = - ordinaryBestMove.isKataData - ? Optional.of(ordinaryBestMove.scoreMean - result.scoreLead()) + ordinaryBestMove.isKataData && result.isKataData + ? Optional.of(ordinaryBestMove.scoreMean - result.scoreMean) : Optional.empty(); return MoveRankDefinition.classifyLosses(winrateLoss, scoreLoss, Lizzie.config) .color(Lizzie.config.useMorandiColors); @@ -3144,80 +3062,25 @@ private void drawTwoRowSuggestionField( } } - private static Color readableTextColor(Color background) { - double luminance = relativeLuminance(background); - double blackContrast = (luminance + 0.05) / 0.05; - double whiteContrast = 1.05 / (luminance + 0.05); - return whiteContrast > blackContrast ? Color.WHITE : Color.BLACK; - } - - private static double relativeLuminance(Color color) { - double red = linearColorComponent(color.getRed() / 255.0); - double green = linearColorComponent(color.getGreen() / 255.0); - double blue = linearColorComponent(color.getBlue() / 255.0); - return 0.2126 * red + 0.7152 * green + 0.0722 * blue; - } - - private static double linearColorComponent(double value) { - return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); - } - - private static Color withOpacity(Color color, int opacityPercent) { - int alpha = (int) Math.round(255 * opacityPercent / 100.0); - return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); - } - - private Color trackingInteriorDisplayColor(Color interior, int centerX, int centerY) { - Color background = Lizzie.config.pureBoardColor; - int sampleX = centerX - x + Math.max(1, stoneRadius / 2); - int sampleY = centerY - y + Math.max(1, stoneRadius / 2); - if (cachedBackgroundImage != emptyImage - && sampleX >= 0 - && sampleX < cachedBackgroundImage.getWidth() - && sampleY >= 0 - && sampleY < cachedBackgroundImage.getHeight()) { - background = new Color(cachedBackgroundImage.getRGB(sampleX, sampleY), true); - } - if (background == null) background = new Color(198, 178, 148); - double alpha = interior.getAlpha() / 255.0; - return new Color( - compositeChannel(interior.getRed(), background.getRed(), alpha), - compositeChannel(interior.getGreen(), background.getGreen(), alpha), - compositeChannel(interior.getBlue(), background.getBlue(), alpha)); - } - - private static int compositeChannel(int foreground, int background, double alpha) { - return (int) Math.round(foreground * alpha + background * (1.0 - alpha)); - } private void drawTrackedPointMarkers( Graphics2D g, - Set trackedSnapshot, - Map trackingResults, + TrackingAnalysisController.DisplaySnapshot snapshot, + List ordinaryMoves, MoveData ordinaryBestMove) { - if (trackedSnapshot.isEmpty() || !Lizzie.config.showTrackingPointOutline) return; + if (snapshot.selectedPoints().isEmpty()) return; Stroke oldStroke = g.getStroke(); Color oldColor = g.getColor(); Composite oldComposite = g.getComposite(); Object oldAntialiasing = g.getRenderingHint(KEY_ANTIALIASING); try { g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); - float ringWidth = Math.max(2.2f, squareWidth * 0.055f); - float dash = Math.max(4.5f, squareWidth * 0.14f); - float gap = Math.max(3.0f, squareWidth * 0.09f); - g.setStroke( - new BasicStroke( - ringWidth, - BasicStroke.CAP_ROUND, - BasicStroke.JOIN_ROUND, - 10.0f, - new float[] {dash, gap}, - 0.0f)); - g.setComposite( - AlphaComposite.getInstance( - AlphaComposite.SRC_OVER, Lizzie.config.trackingPointOutlineOpacityPercent / 100.0f)); - int radius = Math.max(stoneRadius + 2, (int) Math.round(squareWidth * 0.48)); - for (String coordName : trackedSnapshot) { + int qualityRadius = Math.max(stoneRadius + 2, (int) Math.round(squareWidth * 0.48)); + int progressRadius = qualityRadius + Math.max(3, (int) Math.round(squareWidth * 0.075)); + float qualityWidth = Math.max(2.2f, squareWidth * 0.055f); + float progressWidth = Math.max(2.4f, squareWidth * 0.065f); + float qualityAlpha = Lizzie.config.trackingPointOutlineOpacityPercent / 100.0f; + for (String coordName : snapshot.selectedPoints()) { Optional coordsOpt = Board.asCoordinates(coordName); if (!coordsOpt.isPresent()) continue; int[] coords = coordsOpt.get(); @@ -3228,12 +3091,72 @@ private void drawTrackedPointMarkers( if (Lizzie.board != null && !Lizzie.board.iscoordsempty(coords[0], coords[1])) continue; int centerX = x + scaledMarginWidth + squareWidth * coords[0]; int centerY = y + scaledMarginHeight + squareHeight * coords[1]; - TrackingAnalysisController.PointResult result = trackingResults.get(coordName); - g.setColor( - result != null && result.visits() > 0 - ? trackingResultColor(ordinaryBestMove, result) - : new Color(112, 118, 124)); - g.drawOval(centerX - radius, centerY - radius, radius * 2, radius * 2); + MoveData result = null; + for (MoveData candidate : ordinaryMoves) { + if (coordName.equals(candidate.coordinate)) { + result = candidate; + break; + } + } + + if (Lizzie.config.showTrackingPointOutline) { + float dash = Math.max(4.5f, squareWidth * 0.14f); + float gap = Math.max(3.0f, squareWidth * 0.09f); + g.setStroke( + new BasicStroke( + qualityWidth, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 10.0f, + new float[] {dash, gap}, + 0.0f)); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, qualityAlpha)); + g.setColor(trackingResultColor(ordinaryBestMove, result)); + g.drawOval( + centerX - qualityRadius, + centerY - qualityRadius, + qualityRadius * 2, + qualityRadius * 2); + } + + if (snapshot.activePoints().contains(coordName)) { + int visits = snapshot.visits().getOrDefault(coordName, 0); + double ratio = + snapshot.targetVisits() <= 0 + ? 0.0 + : Math.max(0.0, Math.min(1.0, (double) visits / snapshot.targetVisits())); + g.setComposite(AlphaComposite.SrcOver); + g.setStroke( + new BasicStroke( + progressWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.setColor(new Color(25, 118, 210, 105)); + g.drawOval( + centerX - progressRadius, + centerY - progressRadius, + progressRadius * 2, + progressRadius * 2); + g.setColor(new Color(66, 165, 245)); + g.drawArc( + centerX - progressRadius, + centerY - progressRadius, + progressRadius * 2, + progressRadius * 2, + 90, + -(int) Math.round(360.0 * ratio)); + } else if (snapshot.cancellationPending()) { + g.setComposite(AlphaComposite.SrcOver); + g.setStroke( + new BasicStroke( + progressWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.setColor(new Color(245, 166, 35)); + g.drawArc( + centerX - progressRadius, + centerY - progressRadius, + progressRadius * 2, + progressRadius * 2, + 35, + 110); + } } } finally { g.setStroke(oldStroke); @@ -3300,7 +3223,7 @@ private void drawLeelazSuggestionsUnimportant() { int maxPlayouts = 0; double maxWinrate = 0; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); if (move.winrate > maxWinrate) maxWinrate = move.winrate; } for (int i = bestMoves.size() - 1; i >= 0; i--) { @@ -3311,7 +3234,7 @@ private void drawLeelazSuggestionsUnimportant() { continue; // This actually can happen } - float percentPlayouts = (float) move.playouts / maxPlayouts; + float percentPlayouts = (float) move.allocationRatio(maxPlayouts); if (!branchOpt.isPresent()) { Optional coordsOpt = Board.asCoordinates(move.coordinate); @@ -3329,6 +3252,9 @@ private void drawLeelazSuggestionsUnimportant() { if (!outOfOrder && move.order < 20) { continue; } + TrackingAnalysisController.DisplaySnapshot focus = Lizzie.frame.trackingDisplaySnapshot(); + if (boardIndex == 0 && Lizzie.frame.isTrackingDisplayCurrent(focus) + && focus.selectedPoints().contains(move.coordinate)) continue; if (Lizzie.frame.priorityMoveCoords.size() > 0) { boolean needSkip = false; for (String coords2 : Lizzie.frame.priorityMoveCoords) { diff --git a/src/main/java/featurecat/lizzie/gui/ConfigDialog2.java b/src/main/java/featurecat/lizzie/gui/ConfigDialog2.java index 9d53066ae..f82eece5d 100644 --- a/src/main/java/featurecat/lizzie/gui/ConfigDialog2.java +++ b/src/main/java/featurecat/lizzie/gui/ConfigDialog2.java @@ -295,16 +295,9 @@ public class ConfigDialog2 extends JDialog { private JCheckBox chkAutoQuickAnalyzeOnLoad; private JTextField txtTrackingAnalysisMaxVisits; private JCheckBox chkShowTrackingPointOutline; - private ColorLabel lblTrackingPointInteriorColor; - private JSlider sldTrackingPointInteriorOpacity; - private JLabel lblTrackingPointInteriorOpacityValue; - private JPanel pnlTrackingPointInteriorOpacity; private JSlider sldTrackingPointOutlineOpacity; private JLabel lblTrackingPointOutlineOpacityValue; private JPanel pnlTrackingPointOutlineOpacity; - private JCheckBox chkTrackingPointTextAutoColor; - private ColorLabel lblTrackingPointTextColor; - private JPanel pnlTrackingPointTextColor; private JCheckBox chkShowMoveList; private JLabel lblShowMoveNumInVariationPane; private JCheckBox chkShowIndependentMoveList; @@ -1490,27 +1483,12 @@ public void actionPerformed(ActionEvent e) { chkShowTrackingPointOutline = new JCheckBox(); chkShowTrackingPointOutline.setSelected(Lizzie.config.showTrackingPointOutline); - lblTrackingPointInteriorColor = new ColorLabel(this, false); - lblTrackingPointInteriorColor.setColor(Lizzie.config.trackingPointInteriorColor); - sldTrackingPointInteriorOpacity = - new JSlider(0, 100, Lizzie.config.trackingPointInteriorOpacityPercent); - lblTrackingPointInteriorOpacityValue = new JLabel(); - pnlTrackingPointInteriorOpacity = - createPercentSliderControl( - sldTrackingPointInteriorOpacity, lblTrackingPointInteriorOpacityValue); sldTrackingPointOutlineOpacity = new JSlider(0, 100, Lizzie.config.trackingPointOutlineOpacityPercent); lblTrackingPointOutlineOpacityValue = new JLabel(); pnlTrackingPointOutlineOpacity = createPercentSliderControl( sldTrackingPointOutlineOpacity, lblTrackingPointOutlineOpacityValue); - chkTrackingPointTextAutoColor = new JCheckBox(); - chkTrackingPointTextAutoColor.setSelected(Lizzie.config.trackingPointTextAutoColor); - lblTrackingPointTextColor = new ColorLabel(this, false); - lblTrackingPointTextColor.setColor(Lizzie.config.trackingPointTextColor); - pnlTrackingPointTextColor = createColorControl(lblTrackingPointTextColor); - chkTrackingPointTextAutoColor.addItemListener(e -> syncTrackingPointTextColorControl()); - syncTrackingPointTextColorControl(); JLabel lblShowMoveList = new JLabel(resourceBundle.getString("LizzieConfig.lblShowMoveList")); lblShowMoveList.setBounds(608, 78, 173, 15); @@ -3491,49 +3469,24 @@ private void rebuildThemeTabLikeDesign( JPanel trackingAppearance = createDesignSettingsCard( - configText("ConfigDialog2.modern.trackingAppearance.title", "选点评估结果外观"), + configText("ConfigDialog2.modern.trackingAppearance.title", "选点评估关注外框"), configText( "ConfigDialog2.modern.trackingAppearance.subtitle", - "调整评估结果的底色、动态质量外框和文字颜色。")); + "质量外框随当前普通候选更新;计算进度独立显示。")); addToggleRow( trackingAppearance, - configText("ConfigDialog2.modern.trackingAppearance.outline", "显示评估结果外框"), + configText("ConfigDialog2.modern.trackingAppearance.outline", "显示选点评估关注外框"), configText( "ConfigDialog2.modern.trackingAppearance.outlineSub", - "用虚线外框显示等待状态和实时落子质量"), + "只控制质量外框;不会清除关注或改变分析。"), chkShowTrackingPointOutline); - addColorRow( - trackingAppearance, - configText("ConfigDialog2.modern.trackingAppearance.interiorColor", "内部颜色"), - lblTrackingPointInteriorColor); - addComponentRow( - trackingAppearance, - configText("ConfigDialog2.modern.trackingAppearance.interiorOpacity", "内部不透明度"), - configText( - "ConfigDialog2.modern.trackingAppearance.interiorOpacitySub", - "调整固定内部颜色的不透明程度"), - pnlTrackingPointInteriorOpacity); addComponentRow( trackingAppearance, configText("ConfigDialog2.modern.trackingAppearance.outlineOpacity", "外框不透明度"), configText( "ConfigDialog2.modern.trackingAppearance.outlineOpacitySub", - "调整动态质量虚线外框的不透明程度"), + "调整动态质量外框的不透明程度"), pnlTrackingPointOutlineOpacity); - addToggleRow( - trackingAppearance, - configText("ConfigDialog2.modern.trackingAppearance.autoTextColor", "文字颜色自动适配"), - configText( - "ConfigDialog2.modern.trackingAppearance.autoTextColorSub", - "根据内部颜色、不透明度和棋盘背景自动选择黑字或白字"), - chkTrackingPointTextAutoColor); - addComponentRow( - trackingAppearance, - configText("ConfigDialog2.modern.trackingAppearance.textColor", "文字颜色"), - configText( - "ConfigDialog2.modern.trackingAppearance.textColorSub", "关闭自动适配后使用此颜色"), - pnlTrackingPointTextColor); - syncTrackingPointTextColorControl(); content.add(trackingAppearance); content.add(javax.swing.Box.createVerticalStrut(12)); @@ -3658,20 +3611,6 @@ private JPanel createPercentSliderControl(JSlider slider, JLabel valueLabel) { return controls; } - private void syncTrackingPointTextColorControl() { - if (pnlTrackingPointTextColor == null || chkTrackingPointTextAutoColor == null) return; - setComponentTreeEnabled( - pnlTrackingPointTextColor, !chkTrackingPointTextAutoColor.isSelected()); - } - - private void setComponentTreeEnabled(Component component, boolean enabled) { - component.setEnabled(enabled); - if (component instanceof java.awt.Container) { - for (Component child : ((java.awt.Container) component).getComponents()) { - setComponentTreeEnabled(child, enabled); - } - } - } private void addToggleInputRow( JPanel card, @@ -6184,13 +6123,8 @@ private boolean saveConfig() { Lizzie.config.uiConfig.put( "tracking-analysis-max-visits", Lizzie.config.trackingAnalysisMaxVisits); Lizzie.config.showTrackingPointOutline = chkShowTrackingPointOutline.isSelected(); - Lizzie.config.trackingPointInteriorColor = lblTrackingPointInteriorColor.getColor(); - Lizzie.config.trackingPointInteriorOpacityPercent = - sldTrackingPointInteriorOpacity.getValue(); Lizzie.config.trackingPointOutlineOpacityPercent = sldTrackingPointOutlineOpacity.getValue(); - Lizzie.config.trackingPointTextAutoColor = chkTrackingPointTextAutoColor.isSelected(); - Lizzie.config.trackingPointTextColor = lblTrackingPointTextColor.getColor(); Lizzie.config.saveTrackingPointAppearanceConfig(); Lizzie.config.loadSgfLast = chkSgfLoadLast.isSelected(); Lizzie.config.uiConfig.put("load-sgf-last", Lizzie.config.loadSgfLast); diff --git a/src/main/java/featurecat/lizzie/gui/FloatBoardRenderer.java b/src/main/java/featurecat/lizzie/gui/FloatBoardRenderer.java index 37a0ef871..911379a99 100644 --- a/src/main/java/featurecat/lizzie/gui/FloatBoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/FloatBoardRenderer.java @@ -742,10 +742,10 @@ private void drawBranch() { mouseOverTempNode = Lizzie.board.getHistory().getCurOrMainEnd(editMode); int maxPlayouts = 0; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); } - float percentPlayouts = (float) suggestedMove.get().playouts / maxPlayouts; + float percentPlayouts = (float) suggestedMove.get().allocationRatio(maxPlayouts); if (notChangedMouseOverMove) { if (displayedBranchLength == 1) if (!Lizzie.config.autoReplayBranch) return; @@ -1557,7 +1557,7 @@ private void drawLeelazSuggestions(Graphics2D g) { double minWinrate = 100.0; double maxScoreMean = -300; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); if (move.winrate > maxWinrate) maxWinrate = move.winrate; if (move.winrate < minWinrate) minWinrate = move.winrate; if (move.isKataData && Lizzie.config.showScoremeanInSuggestion) { @@ -1577,7 +1577,7 @@ private void drawLeelazSuggestions(Graphics2D g) { continue; // This actually can happen } double fraction = 0; - float percentPlayouts = (float) move.playouts / maxPlayouts; + float percentPlayouts = (float) move.allocationRatio(maxPlayouts); Optional coordsOpt = Board.asCoordinates(move.coordinate); if (!coordsOpt.isPresent()) { @@ -1729,7 +1729,8 @@ private void drawLeelazSuggestions(Graphics2D g) { if (showScoreLead && showPlayouts && showWinrate) { double score = move.scoreMean; boolean shouldShowMaxColorWinrate = canShowMaxColor && hasMaxWinrate; - boolean shouldShowMaxColorPlayouts = canShowMaxColor && move.playouts == maxPlayouts; + boolean shouldShowMaxColorPlayouts = + canShowMaxColor && move.allocationVisits() == maxPlayouts; boolean shouldShowMaxColorScoreLead = canShowMaxColor && move.scoreMean == maxScoreMean; String winrateText = String.format(Locale.ENGLISH, "%.1f", roundedWinrate); @@ -1852,7 +1853,8 @@ private void drawLeelazSuggestions(Graphics2D g) { String winrateText = String.format(Locale.ENGLISH, "%.1f", roundedWinrate); String playoutsText = Utils.getPlayoutsString(move.playouts); boolean shouldShowMaxColorWinrate = canShowMaxColor && hasMaxWinrate; - boolean shouldShowMaxColorPlayouts = canShowMaxColor && move.playouts == maxPlayouts; + boolean shouldShowMaxColorPlayouts = + canShowMaxColor && move.allocationVisits() == maxPlayouts; if (Lizzie.config.useDefaultInfoRowOrder || Lizzie.config.suggestionInfoWinrate < Lizzie.config.suggestionInfoPlayouts) { if (shouldShowMaxColorWinrate) g.setColor(maxColor); @@ -1997,7 +1999,8 @@ private void drawLeelazSuggestions(Graphics2D g) { if (shouldShowMaxColorScoreLead) g.setColor(oriColor); } } else if (showPlayouts && showScoreLead) { - boolean shouldShowMaxColorPlayouts = canShowMaxColor && move.playouts == maxPlayouts; + boolean shouldShowMaxColorPlayouts = + canShowMaxColor && move.allocationVisits() == maxPlayouts; boolean shouldShowMaxColorScoreLead = canShowMaxColor && move.scoreMean == maxScoreMean; double score = move.scoreMean; @@ -2087,7 +2090,8 @@ private void drawLeelazSuggestions(Graphics2D g) { } if (shouldShowMaxColorWinrate) g.setColor(oriColor); } else if (showPlayouts) { - boolean shouldShowMaxColorPlayouts = canShowMaxColor && move.playouts == maxPlayouts; + boolean shouldShowMaxColorPlayouts = + canShowMaxColor && move.allocationVisits() == maxPlayouts; if (shouldShowMaxColorPlayouts) g.setColor(maxColor); drawString( g, @@ -2147,7 +2151,7 @@ private void drawLeelazSuggestionsUnimportant() { int maxPlayouts = 0; double maxWinrate = 0; for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); if (move.winrate > maxWinrate) maxWinrate = move.winrate; } @@ -2159,7 +2163,7 @@ private void drawLeelazSuggestionsUnimportant() { continue; // This actually can happen } - float percentPlayouts = (float) move.playouts / maxPlayouts; + float percentPlayouts = (float) move.allocationRatio(maxPlayouts); if (!branchOpt.isPresent()) { Optional coordsOpt = Board.asCoordinates(move.coordinate); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 2b6cfe7c7..c1e1b0f3d 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -127,238 +127,6 @@ enum ManualAutoAnalysisStartFailure { CANCELLED } - public interface RestartInteractionGate extends AutoCloseable { - @Override - void close(); - } - - public RestartInteractionGate beginRestartInteractionGate() { - return beginRestartInteractionGate(this); - } - - static RestartInteractionGate beginRestartInteractionGate(Window root) { - AtomicReference result = new AtomicReference<>(); - try { - runRestartInteractionMutationOnEdt( - () -> { - List windows = new ArrayList<>(); - collectOwnedWindows( - root, windows, Collections.newSetFromMap(new IdentityHashMap<>())); - Map enabledStates = new IdentityHashMap<>(); - Map transferHandlers = new IdentityHashMap<>(); - for (Window window : windows) { - enabledStates.put(window, window.isEnabled()); - collectTransferHandlers(window, transferHandlers); - } - Component focusOwner = - KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); - KeyboardFocusManager focusManager = - KeyboardFocusManager.getCurrentKeyboardFocusManager(); - KeyEventDispatcher keyboardGate = - event -> { - Component source = event.getComponent(); - Window sourceWindow = - source == null - ? focusManager.getFocusedWindow() - : SwingUtilities.getWindowAncestor(source); - if (sourceWindow != null && windows.contains(sourceWindow)) { - event.consume(); - return true; - } - return false; - }; - try { - focusManager.addKeyEventDispatcher(keyboardGate); - for (JComponent component : transferHandlers.keySet()) { - component.setTransferHandler(null); - } - for (Window window : windows) { - window.setEnabled(false); - } - } catch (RuntimeException | Error failure) { - restoreRestartInteractionState( - windows, - enabledStates, - transferHandlers, - focusManager, - keyboardGate, - focusOwner, - failure); - throw failure; - } - AtomicBoolean closed = new AtomicBoolean(false); - result.set( - () -> { - if (!closed.compareAndSet(false, true)) { - return; - } - runRestartInteractionMutationOnEdt( - () -> - restoreRestartInteractionState( - windows, - enabledStates, - transferHandlers, - focusManager, - keyboardGate, - focusOwner, - null)); - }); - }); - } catch (RuntimeException | Error failure) { - RestartInteractionGate abandonedGate = result.get(); - if (abandonedGate != null) { - boolean restoreInterrupt = Thread.interrupted(); - try { - abandonedGate.close(); - } catch (RuntimeException | Error cleanupFailure) { - addRestartInteractionCleanupFailure(failure, cleanupFailure); - } finally { - if (restoreInterrupt) { - Thread.currentThread().interrupt(); - } - } - } - throw failure; - } - return result.get(); - } - - private static void restoreRestartInteractionState( - List windows, - Map enabledStates, - Map transferHandlers, - KeyboardFocusManager focusManager, - KeyEventDispatcher keyboardGate, - Component focusOwner, - Throwable primaryFailure) { - AtomicReference cleanupFailure = new AtomicReference<>(primaryFailure); - for (Window window : windows) { - runRestartInteractionCleanup( - cleanupFailure, - () -> window.setEnabled(Boolean.TRUE.equals(enabledStates.get(window)))); - } - for (Map.Entry entry : transferHandlers.entrySet()) { - runRestartInteractionCleanup( - cleanupFailure, () -> entry.getKey().setTransferHandler(entry.getValue())); - } - runRestartInteractionCleanup( - cleanupFailure, () -> focusManager.removeKeyEventDispatcher(keyboardGate)); - runRestartInteractionCleanup( - cleanupFailure, - () -> { - if (focusOwner != null && focusOwner.isDisplayable()) { - focusOwner.requestFocusInWindow(); - } - }); - Throwable failure = cleanupFailure.get(); - if (primaryFailure == null && failure != null) { - rethrowRestartInteractionFailure(failure); - } - } - - private static void runRestartInteractionCleanup( - AtomicReference failure, Runnable cleanup) { - try { - cleanup.run(); - } catch (RuntimeException | Error cleanupFailure) { - Throwable primary = failure.get(); - if (primary == null) { - failure.set(cleanupFailure); - } else { - addRestartInteractionCleanupFailure(primary, cleanupFailure); - } - } - } - - private static void addRestartInteractionCleanupFailure( - Throwable primary, Throwable cleanupFailure) { - if (primary != null && cleanupFailure != null && primary != cleanupFailure) { - primary.addSuppressed(cleanupFailure); - } - } - - private static void rethrowRestartInteractionFailure(Throwable failure) { - if (failure instanceof Error) { - throw (Error) failure; - } - throw (RuntimeException) failure; - } - - private static void collectOwnedWindows( - Window window, List windows, Set visited) { - if (window == null || !visited.add(window)) { - return; - } - windows.add(window); - for (Window owned : window.getOwnedWindows()) { - collectOwnedWindows(owned, windows, visited); - } - } - - private static void collectTransferHandlers( - Component component, Map transferHandlers) { - if (component instanceof JComponent) { - JComponent swingComponent = (JComponent) component; - TransferHandler transferHandler = swingComponent.getTransferHandler(); - if (transferHandler != null) { - transferHandlers.put(swingComponent, transferHandler); - } - } - if (component instanceof Container) { - for (Component child : ((Container) component).getComponents()) { - collectTransferHandlers(child, transferHandlers); - } - } - } - - private static void runRestartInteractionMutationOnEdt(Runnable action) { - if (SwingUtilities.isEventDispatchThread()) { - action.run(); - return; - } - AtomicReference actionFailure = new AtomicReference<>(); - CountDownLatch completed = new CountDownLatch(1); - try { - SwingUtilities.invokeLater( - () -> { - try { - action.run(); - } catch (RuntimeException | Error failure) { - actionFailure.set(failure); - } finally { - completed.countDown(); - } - }); - } catch (RuntimeException | Error schedulingFailure) { - throw new IllegalStateException( - "Failed to schedule restart interaction gate update", schedulingFailure); - } - InterruptedException interruption = null; - while (true) { - try { - completed.await(); - break; - } catch (InterruptedException interrupted) { - if (interruption == null) { - interruption = interrupted; - } else if (interruption != interrupted) { - interruption.addSuppressed(interrupted); - } - } - } - Throwable failure = actionFailure.get(); - if (interruption != null) { - if (failure != null && failure != interruption) { - interruption.addSuppressed(failure); - } - Thread.currentThread().interrupt(); - throw new IllegalStateException( - "Interrupted while updating restart interaction gate", interruption); - } - if (failure != null) { - throw new IllegalStateException("Failed to update restart interaction gate", failure); - } - } enum PasteSgfDecision { IGNORE_EMPTY, @@ -824,6 +592,8 @@ enum PasteSgfDecision { private BoardHistoryNode pendingManualAutoAnalysisRoot; private javax.swing.Timer manualAutoAnalysisEngineReadyTimer; private volatile TrackingAnalysisController trackingAnalysisController; + private TrackingAnalysisController.Context trackingVisibilityContext; + private java.util.Set trackingVisibilityPoints = java.util.Set.of(); private boolean redrawWinratePaneOnly = false; private boolean redrawBoardSurfacesOnly = false; private javax.swing.Timer deferredMoveUiRefreshTimer; @@ -3848,7 +3618,18 @@ public void startNewGame() { if (deferUntilHumanSlExit(this::startNewGame)) { return; } - startRetainedEngineMode(RetainedEngineModeTarget.startNewGame(this)); + Leelaz engine = Lizzie.leelaz; + Leelaz.EngineModeReservation reservation = + engine == null ? null : engine.beginEngineModeReservation(); + if (engine != null && reservation == null) { + showForegroundEngineLeaseConflict(); + return; + } + try { + startNewGameReserved(reservation); + } finally { + if (reservation != null) reservation.close(); + } } protected void startNewGameReserved() { @@ -12512,7 +12293,7 @@ public void startAnalyzeGameDialog() { if (deferUntilHumanSlExit(this::startAnalyzeGameDialog)) { return; } - startRetainedEngineMode(RetainedEngineModeTarget.startAnalyzeGame(this)); + runWithForegroundEngineModeReservation(this::startAnalyzeGameDialogReserved); } protected void startAnalyzeGameDialogReserved() { @@ -12568,9 +12349,8 @@ public void continueAiPlaying( () -> continueAiPlaying(isGenmove, continueNow, playerIsB, fromShortCut))) { return; } - startRetainedEngineMode( - RetainedEngineModeTarget.continuePlaying( - this, isGenmove, continueNow, playerIsB, fromShortCut)); + runWithForegroundEngineModeReservation( + () -> continueAiPlayingReserved(isGenmove, continueNow, playerIsB, fromShortCut)); } protected void continueAiPlayingReserved( @@ -13789,10 +13569,7 @@ public String getColumnName(int column) { public Object getValueAt(int row, int col) { - int totalPlayouts = 0; - for (MoveData move : data2) { - totalPlayouts = totalPlayouts + move.playouts; - } + int totalPlayouts = MoveData.getAllocationVisits(data2); if (row > data2.size() - 1) return ""; MoveData data = data2.get(row); switch (col) { @@ -13835,7 +13612,7 @@ else if (data.isNextMove) return Utils.getPlayoutsString(data.playouts); case 4: return String.format( - Locale.ENGLISH, "%.1f", (double) data.playouts * 100 / totalPlayouts); + Locale.ENGLISH, "%.1f", data.allocationRatio(totalPlayouts) * 100); case 5: double score = data.scoreMean; if (EngineGamePresentation.current().playingGenmove()) { @@ -15632,218 +15409,11 @@ boolean runWithForegroundEngineModeReservation(Runnable action) { } } - private void startRetainedEngineMode(RetainedEngineModeTarget target) { - Leelaz currentForegroundEngine = target.engine; - if (currentForegroundEngine == null) { - target.runWithoutTracking(null); - return; - } - Leelaz.TrackingHandoffClaim claim = currentForegroundEngine.claimTrackingHandoff(target); - if (claim.availability() == Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING) { - return; - } - if (claim.availability() != Leelaz.TrackingHandoffAvailability.NOT_TRACKING) { - target.reportConflict(); - return; - } - Leelaz.EngineModeReservation reservation = currentForegroundEngine.beginEngineModeReservation(); - if (reservation == null) { - target.reportConflict(); - return; - } - try { - target.runWithoutTracking(reservation); - } finally { - reservation.close(); - } - } - - private static final class RetainedEngineModeTarget implements Leelaz.TrackingHandoffTarget { - private enum Action { - START_NEW_GAME, - START_ANALYZE_GAME, - CONTINUE_PLAYING - } - - private final LizzieFrame frame; - private final Leelaz engine; - private final BoardHistoryNode historyNode; - private final Zobrist boardPosition; - private final boolean blackToPlay; - private final long contextRevision; - private final Action action; - private final boolean isGenmove; - private final boolean continueNow; - private final boolean playerIsBlack; - private final boolean fromShortCut; - private final java.util.concurrent.atomic.AtomicBoolean settled = - new java.util.concurrent.atomic.AtomicBoolean(false); - - private RetainedEngineModeTarget( - LizzieFrame frame, - Action action, - boolean isGenmove, - boolean continueNow, - boolean playerIsBlack, - boolean fromShortCut) { - this.frame = frame; - this.engine = Lizzie.leelaz; - this.historyNode = - Lizzie.board == null || Lizzie.board.getHistory() == null - ? null - : Lizzie.board.getHistory().getCurrentHistoryNode(); - this.boardPosition = - Lizzie.board == null || Lizzie.board.getHistory() == null - ? null - : Lizzie.board.getHistory().getZobrist(); - this.blackToPlay = - Lizzie.board != null - && Lizzie.board.getHistory() != null - && Lizzie.board.getHistory().isBlacksTurn(); - this.contextRevision = Lizzie.board == null ? 0L : Lizzie.board.getContextRevision(); - this.action = action; - this.isGenmove = isGenmove; - this.continueNow = continueNow; - this.playerIsBlack = playerIsBlack; - this.fromShortCut = fromShortCut; - } - - private static RetainedEngineModeTarget startNewGame(LizzieFrame frame) { - return new RetainedEngineModeTarget(frame, Action.START_NEW_GAME, false, false, false, false); - } - - private static RetainedEngineModeTarget startAnalyzeGame(LizzieFrame frame) { - return new RetainedEngineModeTarget( - frame, Action.START_ANALYZE_GAME, false, false, false, false); - } - - private static RetainedEngineModeTarget continuePlaying( - LizzieFrame frame, - boolean isGenmove, - boolean continueNow, - boolean playerIsBlack, - boolean fromShortCut) { - return new RetainedEngineModeTarget( - frame, Action.CONTINUE_PLAYING, isGenmove, continueNow, playerIsBlack, fromShortCut); - } - - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - if (Lizzie.frame != frame || Lizzie.leelaz != engine) { - return false; - } - BoardHistoryNode currentNode = - Lizzie.board == null || Lizzie.board.getHistory() == null - ? null - : Lizzie.board.getHistory().getCurrentHistoryNode(); - return currentNode == historyNode - && Lizzie.board.getHistory().getZobrist().equals(boardPosition) - && Lizzie.board.getHistory().isBlacksTurn() == blackToPlay - && Lizzie.board.getContextRevision() == contextRevision - && !frame.isPlayingAgainstLeelaz - && !frame.isAnaPlayingAgainstLeelaz; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - if (settled.get()) { - return; - } - if (!callOnEdtAndWait( - () -> { - if (!isCurrent()) { - return false; - } - Leelaz.EngineModeReservation reservation = - activation.beginRetainedEngineModeReservation(); - if (reservation == null) { - return false; - } - try { - runAction(reservation); - } finally { - reservation.close(); - } - return true; - })) { - return; - } - settled.compareAndSet(false, true); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - if (settled.compareAndSet(false, true)) { - runOnEdtAndWait(() -> frame.showRetainedEngineModeActivationFailure(failure)); - } - } - - private void runWithoutTracking(Leelaz.EngineModeReservation reservation) { - if (settled.compareAndSet(false, true)) { - runAction(reservation); - } - } - - private void runAction(Leelaz.EngineModeReservation reservation) { - switch (action) { - case START_NEW_GAME: - frame.startNewGameReserved(reservation); - break; - case START_ANALYZE_GAME: - frame.startAnalyzeGameDialogReserved(); - break; - case CONTINUE_PLAYING: - frame.continueAiPlayingReserved(isGenmove, continueNow, playerIsBlack, fromShortCut); - break; - } - } - - private void reportConflict() { - if (action == Action.START_NEW_GAME) { - frame.showForegroundEngineLeaseConflict(); - } else { - frame.showForegroundEngineModeReservationConflict(); - } - } - - private static void runOnEdtAndWait(Runnable action) { - if (SwingUtilities.isEventDispatchThread()) { - action.run(); - return; - } - try { - SwingUtilities.invokeAndWait(action); - } catch (Exception failure) { - throw new IllegalStateException(failure); - } - } - - private static boolean callOnEdtAndWait(BooleanSupplier action) { - if (SwingUtilities.isEventDispatchThread()) { - return action.getAsBoolean(); - } - AtomicBoolean result = new AtomicBoolean(false); - runOnEdtAndWait(() -> result.set(action.getAsBoolean())); - return result.get(); - } - } protected void showForegroundEngineModeReservationConflict() { Utils.showMsg(Lizzie.resourceBundle.getString("AnalysisSettings.reuseStatus.existing_lease")); } - protected void showRetainedEngineModeActivationFailure(Leelaz.TrackingHandoffFailure failure) { - String key = - failure == Leelaz.TrackingHandoffFailure.CONTEXT_INVALIDATED - ? "AnalysisSettings.reuseStatus.not_current_foreground_engine" - : "AnalysisSettings.reuseStatus.engine_state_unrestored"; - Utils.showMsg(Lizzie.resourceBundle.getString(key)); - } public TrackingAnalysisController trackingAnalysisController() { TrackingAnalysisController controller = trackingAnalysisController; @@ -15852,18 +15422,32 @@ public TrackingAnalysisController trackingAnalysisController() { } synchronized (this) { if (trackingAnalysisController == null) { - trackingAnalysisController = new TrackingAnalysisController(this::requestAnalysisRefresh); + trackingAnalysisController = new TrackingAnalysisController(this::onTrackingDisplayChanged); } return trackingAnalysisController; } } + private void onTrackingDisplayChanged() { + TrackingAnalysisController.DisplaySnapshot snapshot = trackingAnalysisController.snapshot(); + if (trackingVisibilityContext != snapshot.context() + || !snapshot.selectedPoints().equals(trackingVisibilityPoints)) { + trackingVisibilityContext = snapshot.context(); + trackingVisibilityPoints = snapshot.selectedPoints(); + if (isTrackingDisplayCurrent(snapshot)) { + getDisplayNode().getData().refreshFocusCandidateVisibility(); + } + } + requestAnalysisRefresh(); + } + public TrackingAnalysisController.AddResult addTrackingPoint(String coordinate) { TrackingAnalysisController.Context context = currentTrackingContext(); if (context == null) { return TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE; } TrackingAnalysisController controller = trackingAnalysisController(); + controller.contextChanged(context); if (readBoard == null) { return controller.addPoint(coordinate, context); } @@ -15906,6 +15490,7 @@ public boolean canStartTrackingAnalysis() { if (Lizzie.board == null || Lizzie.leelaz == null || !Lizzie.leelaz.isEligibleLocalKataGoForReadBoardTracking() + || !Lizzie.leelaz.canStartMoveFocus() || Lizzie.board.getHistory() == null) { return false; } diff --git a/src/main/java/featurecat/lizzie/gui/RightClickMenu.java b/src/main/java/featurecat/lizzie/gui/RightClickMenu.java index 2592b0c9f..061fc9c1f 100644 --- a/src/main/java/featurecat/lizzie/gui/RightClickMenu.java +++ b/src/main/java/featurecat/lizzie/gui/RightClickMenu.java @@ -178,7 +178,18 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent e) { RightClickMenu.coords[0], RightClickMenu.coords[1]); boolean isTracked = Lizzie.frame.isTrackingPoint(coordName); boolean hasTracked = Lizzie.frame.hasTrackingPoints(); - trackPoint.setVisible(canTrack && !isTracked); + trackPoint.setVisible(!isTracked && Lizzie.board.iscoordsempty(coords[0], coords[1])); + trackPoint.setEnabled(canTrack); + trackPoint.setText( + resourceBundle.getString( + canTrack + ? "RightClickMenu.trackPoint" + : "RightClickMenu.trackPoint.requiresCapability")); + trackPoint.setToolTipText( + resourceBundle.getString( + canTrack + ? "RightClickMenu.trackPoint.tooltip" + : "RightClickMenu.trackPoint.unavailableTooltip")); untrackPoint.setVisible(isTracked); clearAllTracked.setVisible(hasTracked); } @@ -249,6 +260,7 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent e) { cancelavoid.setIcon(iconRecycle); trackPoint = new JFontMenuItem(resourceBundle.getString("RightClickMenu.trackPoint")); trackPoint.setIcon(iconTrackPoint); + trackPoint.setToolTipText(resourceBundle.getString("RightClickMenu.trackPoint.tooltip")); untrackPoint = new JFontMenuItem(resourceBundle.getString("RightClickMenu.untrackPoint")); untrackPoint.setIcon(iconRemovePoint); clearAllTracked = new JFontMenuItem(resourceBundle.getString("RightClickMenu.clearAllTracked")); diff --git a/src/main/java/featurecat/lizzie/gui/SubBoardRenderer.java b/src/main/java/featurecat/lizzie/gui/SubBoardRenderer.java index 420844de3..53cc6cc44 100644 --- a/src/main/java/featurecat/lizzie/gui/SubBoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/SubBoardRenderer.java @@ -1272,7 +1272,7 @@ public int compare(MoveData s1, MoveData s2) { } } for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.allocationVisits() > maxPlayouts) maxPlayouts = move.allocationVisits(); if (move.policy > maxPolicy) maxPolicy = move.policy; // if (move.winrate < minWinrate) minWinrate = move.winrate; @@ -1311,7 +1311,7 @@ public int compare(MoveData s1, MoveData s2) { int length = 0; if (showHeatAfterCalc) - length = (int) ((move.playouts * 2.7f * squareWidth) / maxPlayouts); + length = (int) (move.allocationRatio(maxPlayouts) * 2.7f * squareWidth); else length = (int) ((move.policy * 2.7f * squareWidth) / maxPolicy); drawHeat(g, suggestionX - length / 2, suggestionY - length / 2, length); } diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index 3562c3ebc..ae45303ef 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -4184,7 +4184,7 @@ private HistoryNavigationRestore prepareHistoryNavigationRestore(boolean stepIn) Leelaz engine = Lizzie.leelaz; return prepareHistoryNavigationRestore( stepIn, - engine != null && engine.isPonderingOrWasPonderingBeforeTracking() ? engine::ponder : null); + engine != null && engine.isPondering() ? engine::ponder : null); } private HistoryNavigationRestore prepareHistoryNavigationRestore( @@ -4416,7 +4416,7 @@ void restoreHistoryNodeExact(BoardHistoryNode node) { HistoryNavigationRestore restore; synchronized (this) { if (!submitOrdinaryEngineForwarding(engine, () -> true)) return; - boolean resumePonder = engine.isPonderingOrWasPonderingBeforeTracking(); + boolean resumePonder = engine.isPondering(); ExactSnapshotEngineRestore.PreparedRestore exact = ExactSnapshotEngineRestore.prepare( engine.captureHistoryNavigationExactSnapshotRestoreAdmission(mirror), node) @@ -4453,7 +4453,7 @@ private void restoreEnginePosition( HistoryNavigationRestore restore; synchronized (this) { if (!submitOrdinaryEngineForwarding(engine, () -> true)) return; - boolean resumePonder = engine.isPonderingOrWasPonderingBeforeTracking(); + boolean resumePonder = engine.isPondering(); Optional exact = fromRoot ? Optional.empty() diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index f9c9b123e..de148b38d 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -35,6 +35,64 @@ public class BoardData { public List bestMovesOutOfRange; public List bestMoves2; public List bestMoves2OutOfRange; + /** Exact root counts; -1 denotes legacy/unknown totals in the corresponding slot. */ + public int rootVisits = -1; + public int rootVisits2 = -1; + private Object analysisSource; + private Object analysisSource2; + + public enum AnalysisAdoption { REJECTED, OWNERSHIP_ONLY, FULL } + + /** Called only after the engine has revalidated the exact ordinary output owner. */ + public AnalysisAdoption adoptOrdinaryAnalysis( + List moves, String engineName, Leelaz engine, int totalVisits, + int exactRootVisits, List ownership, Object source, boolean secondary, + boolean forceFull) { + if (moves == null || moves.isEmpty()) return AnalysisAdoption.REJECTED; + int cached = secondary ? playouts2 : playouts; + int cachedRoot = secondary ? rootVisits2 : rootVisits; + Object cachedSource = secondary ? analysisSource2 : analysisSource; + boolean invalidated = secondary ? isChanged2 : isChanged; + double cachedPda = secondary ? pda2 : pda; + boolean sameSource = source != null && source == cachedSource; + boolean comparable = cachedRoot < 0 || exactRootVisits >= 0; + int comparisonVisits = cachedRoot < 0 && exactRootVisits >= 0 + ? MoveData.getPlayouts(moves) : totalVisits; + boolean protectedCache = Lizzie.config.enableLizzieCache && !Lizzie.config.isAutoAna + && (secondary || !Lizzie.engineGame.current().playing()); + boolean full = forceFull || !protectedCache || invalidated || cachedPda != engine.pda + || (comparable && comparisonVisits > cached) + || (sameSource && cachedRoot >= 0 && exactRootVisits >= 0 && totalVisits == cached); + if (!full) { + List oldOwnership = secondary ? estimateArray2 : estimateArray; + if (ownership != null && !ownership.isEmpty() + && (!comparable || comparisonVisits < cached || oldOwnership == null)) { + if (secondary) estimateArray2 = compactEstimateArray(ownership); + else estimateArray = compactEstimateArray(ownership); + return AnalysisAdoption.OWNERSHIP_ONLY; + } + return AnalysisAdoption.REJECTED; + } + List retainedOwnership = ownership; + if (sameSource && (ownership == null || ownership.isEmpty())) { + retainedOwnership = secondary ? estimateArray2 : estimateArray; + } + moves.sort(Comparator.comparingInt(move -> move.order)); + if (secondary) { + tryToSetBestMoves2FromEngine( + moves, engineName, true, engine, totalVisits, retainedOwnership, true); + rootVisits2 = exactRootVisits; + analysisSource2 = source; + isChanged2 = false; + } else { + tryToSetBestMovesFromEngine( + moves, engineName, engine, totalVisits, retainedOwnership, true); + rootVisits = exactRootVisits; + analysisSource = source; + isChanged = false; + } + return AnalysisAdoption.FULL; + } public int blackCaptures; public int whiteCaptures; public boolean isChanged = false; @@ -496,15 +554,34 @@ public int compare(MoveData s1, MoveData s2) { return true; } + /** Reapplies only candidate visibility after attention changes; analysis values stay intact. */ + public void refreshFocusCandidateVisibility() { + if (bestMovesOutOfRange != null && !bestMovesOutOfRange.isEmpty()) { + List complete = new ArrayList<>(bestMoves.size() + bestMovesOutOfRange.size()); + complete.addAll(bestMoves); + complete.addAll(bestMovesOutOfRange); + complete.sort(Comparator.comparingInt(move -> move.order)); + bestMoves = complete; + } else { + bestMoves = new ArrayList<>(bestMoves); + } + bestMovesOutOfRange = new ArrayList<>(); + tryToLimitMoves(bestMoves, List.of(), true); + } + private void tryToLimitMoves(List moves, List lastMoves, boolean isMain) { // TODO Auto-generated method stub List outOfRangeMoves = new ArrayList<>(); + featurecat.lizzie.analysis.TrackingAnalysisController.DisplaySnapshot focus = + isMain && Lizzie.frame != null ? Lizzie.frame.trackingDisplaySnapshot() : null; + Set attended = focus != null && Lizzie.frame.isTrackingDisplayCurrent(focus) + && Lizzie.frame.getDisplayNode().getData() == this ? focus.selectedPoints() : Set.of(); if (Lizzie.config.limitMaxSuggestion > 0 && !Lizzie.config.showNoSuggCircle && (moves.size() > Lizzie.config.limitMaxSuggestion)) { for (int n = Lizzie.config.limitMaxSuggestion; n < moves.size(); n++) { MoveData move = moves.get(n); - boolean needSkip = false; + boolean needSkip = attended.contains(move.coordinate); int absoluteMaxSuggestionOrder = Lizzie.config.limitMaxSuggestion + 1; if (move.order < absoluteMaxSuggestionOrder) { for (int s = 0; s < absoluteMaxSuggestionOrder && s < lastMoves.size(); s++) { @@ -577,7 +654,14 @@ private void tryToSetBestMoves2FromEngine( Leelaz metadataEngine, int totalplayouts, List estimateArray) { - if (Lizzie.config.enableLizzieCache && !Lizzie.config.isAutoAna) { + tryToSetBestMoves2FromEngine( + moves, engName, isFromLeelaz, metadataEngine, totalplayouts, estimateArray, false); + } + + private void tryToSetBestMoves2FromEngine( + List moves, String engName, boolean isFromLeelaz, + Leelaz metadataEngine, int totalplayouts, List estimateArray, boolean forceOverride) { + if (!forceOverride && Lizzie.config.enableLizzieCache && !Lizzie.config.isAutoAna) { if (!(totalplayouts > playouts2 || isChanged2 || (metadataEngine != null && pda2 != metadataEngine.pda))) { // ||Lizzie.frame.urlSgf @@ -788,6 +872,8 @@ public String bestMovesToString() { sb.append(" prior ").append((int) (move.policy * 100)); if (isKataData) sb.append(" scoreMean ").append(String.format(Locale.ENGLISH, "%.2f", move.scoreMean)); + sb.append(" order ").append(move.order); + if (move.edgeVisits >= 0) sb.append(" edgeVisits ").append(move.edgeVisits); sb.append(" pv ") .append( move.variation == null @@ -815,6 +901,8 @@ public String bestMovesToString2() { sb.append(" winrate ").append((int) (move.winrate * 100)); sb.append(" prior ").append((int) (move.policy * 100)); if (isKataData2) sb.append(" scoreMean ").append(move.scoreMean); + sb.append(" order ").append(move.order); + if (move.edgeVisits >= 0) sb.append(" edgeVisits ").append(move.edgeVisits); sb.append(" pv ").append(move.variation.stream().reduce((a, b) -> a + " " + b).get()); sb.append(" info "); // this order is just because of how the MoveData info parser works } @@ -824,12 +912,16 @@ public String bestMovesToString2() { public void setPlayouts(int playouts) { // if (playouts > this.playouts || isChanged) { this.playouts = playouts; + rootVisits = -1; + analysisSource = null; // } } public void setPlayouts2(int playouts) { // if (playouts > this.playouts || isChanged) { this.playouts2 = playouts; + rootVisits2 = -1; + analysisSource2 = null; // } } @@ -846,7 +938,7 @@ public void setScoreMean2(double scoreMean) { } public void setPlayoutsForce(int playouts) { - this.playouts = playouts; + setPlayouts(playouts); } public int getPlayouts() { @@ -1014,6 +1106,11 @@ private void copyAnalysisStateFrom(BoardData data, boolean includeComment) { this.winrate2 = data.winrate2; this.playouts = data.playouts; this.playouts2 = data.playouts2; + this.rootVisits = data.rootVisits; + this.rootVisits2 = data.rootVisits2; + // Copies preserve saved analysis, not permission to refresh it from a live stream. + this.analysisSource = null; + this.analysisSource2 = null; this.scoreMean = data.scoreMean; this.scoreMean2 = data.scoreMean2; this.scoreStdev = data.scoreStdev; diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index 10ca1f821..0703e3fc8 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -2784,7 +2784,7 @@ private static String formatNodeData(BoardHistoryNode node) { data.scoreStdev, data.pda, data.bestMovesToString(), - data.estimateArray); + data.estimateArray, data.rootVisits); } private static String formatNodeData2(BoardHistoryNode node) { @@ -2814,7 +2814,7 @@ private static String formatNodeData2(BoardHistoryNode node) { data.scoreStdev2, data.pda2, data.bestMovesToString2(), - data.estimateArray2); + data.estimateArray2, data.rootVisits2); } private static String formatAnalysisPayload( @@ -2827,13 +2827,14 @@ private static String formatAnalysisPayload( double scoreStdev, double pda, String bestMoves, - List estimateArray) { + List estimateArray, int rootVisits) { StringBuilder header = new StringBuilder(); header.append(engineName).append(" ").append(curWinrate).append(" ").append(playouts); - if (kataData) { - boolean hasExplicitHeaderSlots = headerSlots > 0; + if (kataData || rootVisits >= 0) { + boolean hasExplicitHeaderSlots = rootVisits < 0 && headerSlots > 0; boolean includeScoreStdev = !hasExplicitHeaderSlots || headerSlots >= 5; - boolean includePda = hasExplicitHeaderSlots ? headerSlots >= 6 : Double.compare(pda, 0) != 0; + boolean includePda = rootVisits >= 0 + || (hasExplicitHeaderSlots ? headerSlots >= 6 : Double.compare(pda, 0) != 0); header.append(" ").append(formatAnalysisScalar(scoreMean)); if (includeScoreStdev) { header.append(" ").append(formatAnalysisScalar(scoreStdev)); @@ -2842,6 +2843,7 @@ private static String formatAnalysisPayload( header.append(" ").append(formatAnalysisScalar(pda)); } } + if (rootVisits >= 0) header.append(" rootVisits=").append(rootVisits); String detailLine = formatAnalysisDetailLine(bestMoves, estimateArray); if (Utils.isBlank(detailLine)) { @@ -3191,6 +3193,7 @@ private static void movePrimaryAnalysisPayloadToSnapshot( snapshotData.engineName = moveData.engineName; snapshotData.winrate = moveData.winrate; snapshotData.setPlayouts(moveData.getPlayouts()); + snapshotData.rootVisits = moveData.rootVisits; snapshotData.bestMoves = copyMoveDataListAllowNull(moveData.bestMoves); snapshotData.bestMovesOutOfRange = copyMoveDataListAllowNull(moveData.bestMovesOutOfRange); snapshotData.estimateArray = copyEstimateArray(moveData.estimateArray); @@ -3213,6 +3216,7 @@ private static void moveSecondaryAnalysisPayloadToSnapshot( snapshotData.engineName2 = moveData.engineName2; snapshotData.winrate2 = moveData.winrate2; snapshotData.setPlayouts2(moveData.getPlayouts2()); + snapshotData.rootVisits2 = moveData.rootVisits2; snapshotData.bestMoves2 = copyMoveDataListAllowNull(moveData.bestMoves2); snapshotData.bestMoves2OutOfRange = copyMoveDataListAllowNull(moveData.bestMoves2OutOfRange); snapshotData.estimateArray2 = copyEstimateArray(moveData.estimateArray2); @@ -4413,7 +4417,7 @@ private static void applyAnalysisTagOnDetachedHistory( String line2 = lines.length > 1 ? lines[1] : ""; boolean secondaryEngine = isSecondaryEngineAnalysisTag(tag); applyAnalysisHeader(target, line1, secondaryEngine); - int numPlayouts = parseAnalysisPlayouts(line1[2]); + int numPlayouts = secondaryEngine ? target.getPlayouts2() : target.getPlayouts(); if (numPlayouts <= 0 || Utils.isBlank(line2)) { return; } @@ -4425,11 +4429,23 @@ private static void applyAnalysisTagOnDetachedHistory( private static void applyAnalysisHeader( BoardData target, String[] line1, boolean secondaryEngineTag) { int headerPlayouts = parseAnalysisPlayouts(line1[2]); - int headerSlots = line1.length; + int headerSlots = Math.min(6, line1.length); + int rootVisits = -1; + for (int i = 6; i < line1.length; i++) { + if (line1[i].startsWith("rootVisits=")) { + try { + rootVisits = Integer.parseInt(line1[i].substring("rootVisits=".length())); + } catch (NumberFormatException ignored) { + rootVisits = -1; + } + } + } + if (rootVisits >= 0) headerPlayouts = rootVisits; if (secondaryEngineTag) { target.engineName2 = line1[0]; target.winrate2 = 100 - parseDoubleOrDefault(line1[1], 50); target.setPlayouts2(headerPlayouts); + target.rootVisits2 = rootVisits; target.isKataData2 = false; target.analysisHeaderSlots2 = headerSlots; target.scoreMean2 = 0; @@ -4450,6 +4466,7 @@ private static void applyAnalysisHeader( target.engineName = line1[0]; target.winrate = 100 - parseDoubleOrDefault(line1[1], 50); target.setPlayouts(headerPlayouts); + target.rootVisits = rootVisits; target.isKataData = false; target.analysisHeaderSlots = headerSlots; target.scoreMean = 0; @@ -4539,6 +4556,7 @@ private static ParsedAnalysisInfo parseAnalysisInfoForDetachedNode( private static MoveData parseMoveDataFromAnalysis(String variation, List bestMoves) { MoveData result = new MoveData(); + result.order = bestMoves.size(); String[] data = variation.trim().split("\\s+"); for (int i = 0; i < data.length - 1; i++) { String key = data[i]; @@ -4549,7 +4567,6 @@ private static MoveData parseMoveDataFromAnalysis(String variation, List(); @@ -4580,6 +4597,10 @@ private static void applyMoveDataToken(MoveData target, String key, String value target.coordinate = value; } else if ("visits".equals(key)) { target.playouts = parseAnalysisPlayouts(value); + } else if ("order".equals(key)) { + target.order = Integer.parseInt(value); + } else if ("edgeVisits".equals(key)) { + target.edgeVisits = Integer.parseInt(value); } else if ("winrate".equals(key)) { target.winrate = parseAnalysisPlayouts(value) / 100.0; target.lcb = target.winrate; diff --git a/src/main/java/featurecat/lizzie/util/KataGoRuntimeHelper.java b/src/main/java/featurecat/lizzie/util/KataGoRuntimeHelper.java index cb2c61b79..cde009279 100644 --- a/src/main/java/featurecat/lizzie/util/KataGoRuntimeHelper.java +++ b/src/main/java/featurecat/lizzie/util/KataGoRuntimeHelper.java @@ -3998,7 +3998,7 @@ public static BenchmarkPauseResult pauseCurrentAnalysisForBenchmark() { boolean analysisWasPondering = currentEngine != null && currentEngine.isLoaded() - && currentEngine.isPonderingOrWasPonderingBeforeTracking(); + && currentEngine.isPondering(); Leelaz.ExclusiveGtpLifecycleReservation reservation = currentEngine == null ? null : currentEngine.beginExclusiveGtpLifecycleReservation(); if (currentEngine != null && reservation == null) { diff --git a/src/main/java/featurecat/lizzie/util/Utils.java b/src/main/java/featurecat/lizzie/util/Utils.java index f9eb14ae5..6ceca0397 100644 --- a/src/main/java/featurecat/lizzie/util/Utils.java +++ b/src/main/java/featurecat/lizzie/util/Utils.java @@ -1393,6 +1393,7 @@ public static List getBestMovesFromJsonArray( mv.order = moveInfo.getInt("order"); mv.coordinate = moveInfo.getString("move"); mv.playouts = moveInfo.getInt("visits"); + mv.edgeVisits = moveInfo.optInt("edgeVisits", -1); mv.winrate = moveInfo.getDouble("winrate") * 100; // mv.oriwinrate = mv.winrate; mv.lcb = moveInfo.getDouble("lcb") * 100; diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index 2367895d8..c4acdfe09 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=Previous LizzieConfig.lblLoadKomi=Load Komi From SGF LizzieConfig.lblKifuLoadLast=Jump To End After Load Game LizzieConfig.lblAutoQuickAnalyzeOnLoad=Quick Winrate Graph After Load -LizzieConfig.lblTrackingMaxVisits=\u9009\u70B9\u8BC4\u4F30\u8BA1\u7B97\u91CF +LizzieConfig.lblTrackingMaxVisits=Move-focus cumulative visit target LizzieConfig.lblEnableDragStone=Drag Stone LizzieConfig.lblEnableClickReview=Click Stone Review LizzieConfig.visits=V @@ -2370,9 +2370,12 @@ RightClickMenu.avoid=Avoid RightClickMenu.avoid2=Keep in allow/avoid RightClickMenu.previousMove=Previous move RightClickMenu.cancelavoid=Cancel avoid/allow -RightClickMenu.trackPoint=Track this point -RightClickMenu.untrackPoint=Untrack this point -RightClickMenu.clearAllTracked=Clear all tracked +RightClickMenu.trackPoint=Evaluate this point +RightClickMenu.trackPoint.tooltip=Updates this position's ordinary analysis until the candidate reaches the cumulative visit target +RightClickMenu.trackPoint.unavailableTooltip=Requires a stable position and the current foreground local KataGo with move-focus capability +RightClickMenu.trackPoint.requiresCapability=Move focus unavailable (local KataGo capability required) +RightClickMenu.untrackPoint=Cancel evaluation +RightClickMenu.clearAllTracked=Clear all evaluation points RightClickMenu.cleanedittemp=Clear edit cache RightClickMenu.cleanupedit=Undo RightClickMenu.clearPriority=Clear priority @@ -3166,8 +3169,8 @@ ConfigDialog2.modern.analysis.graphFill=Use area fill in single-curve mode ConfigDialog2.modern.analysis.graphFillSub=Fill from the single visible curve to the 50% / 0 midline ConfigDialog2.modern.analysis.maxRed=Highlight the highest candidate value in red ConfigDialog2.modern.analysis.maxRedSub=When disabled, values on candidate markers use regular black or white text -ConfigDialog2.modern.analysis.trackingVisits=Tracking point visits -ConfigDialog2.modern.analysis.trackingVisitsSub=Stop analyzing each tracked point after it reaches this visit count +ConfigDialog2.modern.analysis.trackingVisits=Move-focus cumulative visit target +ConfigDialog2.modern.analysis.trackingVisitsSub=Add focus in ordinary analysis until each candidate reaches this cumulative visit count; ordinary limits remain unchanged ConfigDialog2.modern.unit.visits=visits ConfigDialog2.modern.analysis.suggestionLimit=Maximum candidate moves ConfigDialog2.modern.analysis.suggestionLimitSub=Limit the candidate moves shown on the main board @@ -3182,19 +3185,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=Pause live analysis after this many s ConfigDialog2.modern.unit.seconds=seconds ConfigDialog2.modern.analysis.limitVisits=Limit visits ConfigDialog2.modern.analysis.limitVisitsSub=Pause live analysis after this many visits. Uncheck for unlimited. -ConfigDialog2.modern.trackingAppearance.title=Tracking point appearance -ConfigDialog2.modern.trackingAppearance.subtitle=Adjust the result interior, live quality outline, and text colors. -ConfigDialog2.modern.trackingAppearance.outline=Show tracking point outline -ConfigDialog2.modern.trackingAppearance.outlineSub=Use a dashed outline for pending state and live move quality -ConfigDialog2.modern.trackingAppearance.interiorColor=Interior color -ConfigDialog2.modern.trackingAppearance.interiorOpacity=Interior opacity -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=Adjust how opaque the fixed interior color appears on the board +ConfigDialog2.modern.trackingAppearance.title=Attention quality outline +ConfigDialog2.modern.trackingAppearance.subtitle=Quality follows the current ordinary candidate versus the order-1 candidate; activity and progress are shown separately. +ConfigDialog2.modern.trackingAppearance.outline=Show attention quality outline +ConfigDialog2.modern.trackingAppearance.outlineSub=Controls only the quality outline; attention and analysis are unchanged ConfigDialog2.modern.trackingAppearance.outlineOpacity=Outline opacity -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=Adjust the opacity of the live quality dashed outline -ConfigDialog2.modern.trackingAppearance.autoTextColor=Automatic text color -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=Choose black or white from the interior color, opacity, and board background -ConfigDialog2.modern.trackingAppearance.textColor=Text color -ConfigDialog2.modern.trackingAppearance.textColorSub=Used when automatic text color is turned off +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=Adjust the opacity of the dynamic quality outline ConfigDialog2.modern.play.title=Play & review controls ConfigDialog2.modern.play.subtitle=Configure board markers, pointer actions, and review interactions. ConfigDialog2.modern.play.doubleClick=Enable double-click move lookup diff --git a/src/main/resources/l10n/DisplayStrings_en_US.properties b/src/main/resources/l10n/DisplayStrings_en_US.properties index 029faefbd..aa495fc7a 100644 --- a/src/main/resources/l10n/DisplayStrings_en_US.properties +++ b/src/main/resources/l10n/DisplayStrings_en_US.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=Previous LizzieConfig.lblLoadKomi=Load Komi From SGF LizzieConfig.lblKifuLoadLast=Jump To End After Load Game LizzieConfig.lblAutoQuickAnalyzeOnLoad=Quick Winrate Graph After Load -LizzieConfig.lblTrackingMaxVisits=Tracking Max Visits +LizzieConfig.lblTrackingMaxVisits=Move-focus cumulative visit target LizzieConfig.lblEnableDragStone=Drag Stone LizzieConfig.lblEnableClickReview=Click Stone Review LizzieConfig.visits=V @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=Avoid RightClickMenu.avoid2=Keep in allow/avoid RightClickMenu.previousMove=Previous move RightClickMenu.cancelavoid=Cancel avoid/allow -RightClickMenu.trackPoint=Track this point -RightClickMenu.untrackPoint=Untrack this point -RightClickMenu.clearAllTracked=Clear all tracked +RightClickMenu.trackPoint=Evaluate this point +RightClickMenu.trackPoint.tooltip=Updates this position's ordinary analysis until the candidate reaches the cumulative visit target +RightClickMenu.trackPoint.unavailableTooltip=Requires a stable position and the current foreground local KataGo with move-focus capability +RightClickMenu.trackPoint.requiresCapability=Move focus unavailable (local KataGo capability required) +RightClickMenu.untrackPoint=Cancel evaluation +RightClickMenu.clearAllTracked=Clear all evaluation points RightClickMenu.cleanedittemp=Clear edit cache RightClickMenu.cleanupedit=Undo RightClickMenu.clearPriority=Clear priority @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=Use area fill in single-curve mode ConfigDialog2.modern.analysis.graphFillSub=Fill from the single visible curve to the 50% / 0 midline ConfigDialog2.modern.analysis.maxRed=Highlight the highest candidate value in red ConfigDialog2.modern.analysis.maxRedSub=When disabled, values on candidate markers use regular black or white text -ConfigDialog2.modern.analysis.trackingVisits=Tracking point visits -ConfigDialog2.modern.analysis.trackingVisitsSub=Stop analyzing each tracked point after it reaches this visit count +ConfigDialog2.modern.analysis.trackingVisits=Move-focus cumulative visit target +ConfigDialog2.modern.analysis.trackingVisitsSub=Add focus in ordinary analysis until each candidate reaches this cumulative visit count; ordinary limits remain unchanged ConfigDialog2.modern.unit.visits=visits ConfigDialog2.modern.analysis.suggestionLimit=Maximum candidate moves ConfigDialog2.modern.analysis.suggestionLimitSub=Limit the candidate moves shown on the main board @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=Pause live analysis after this many s ConfigDialog2.modern.unit.seconds=seconds ConfigDialog2.modern.analysis.limitVisits=Limit visits ConfigDialog2.modern.analysis.limitVisitsSub=Pause live analysis after this many visits. Uncheck for unlimited. -ConfigDialog2.modern.trackingAppearance.title=Tracking point appearance -ConfigDialog2.modern.trackingAppearance.subtitle=Adjust the result interior, live quality outline, and text colors. -ConfigDialog2.modern.trackingAppearance.outline=Show tracking point outline -ConfigDialog2.modern.trackingAppearance.outlineSub=Use a dashed outline for pending state and live move quality -ConfigDialog2.modern.trackingAppearance.interiorColor=Interior color -ConfigDialog2.modern.trackingAppearance.interiorOpacity=Interior opacity -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=Adjust how opaque the fixed interior color appears on the board +ConfigDialog2.modern.trackingAppearance.title=Attention quality outline +ConfigDialog2.modern.trackingAppearance.subtitle=Quality follows the current ordinary candidate versus the order-1 candidate; activity and progress are shown separately. +ConfigDialog2.modern.trackingAppearance.outline=Show attention quality outline +ConfigDialog2.modern.trackingAppearance.outlineSub=Controls only the quality outline; attention and analysis are unchanged ConfigDialog2.modern.trackingAppearance.outlineOpacity=Outline opacity -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=Adjust the opacity of the live quality dashed outline -ConfigDialog2.modern.trackingAppearance.autoTextColor=Automatic text color -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=Choose black or white from the interior color, opacity, and board background -ConfigDialog2.modern.trackingAppearance.textColor=Text color -ConfigDialog2.modern.trackingAppearance.textColorSub=Used when automatic text color is turned off +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=Adjust the opacity of the dynamic quality outline ConfigDialog2.modern.play.title=Play & review controls ConfigDialog2.modern.play.subtitle=Configure board markers, pointer actions, and review interactions. ConfigDialog2.modern.play.doubleClick=Enable double-click move lookup diff --git a/src/main/resources/l10n/DisplayStrings_ja_JP.properties b/src/main/resources/l10n/DisplayStrings_ja_JP.properties index 404e514c2..186f5909f 100644 --- a/src/main/resources/l10n/DisplayStrings_ja_JP.properties +++ b/src/main/resources/l10n/DisplayStrings_ja_JP.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=\u4E00\u624B\u623B\u308B LizzieConfig.lblLoadKomi=SGF\u306E\u30B3\u30DF\u3092\u9069\u7528 LizzieConfig.lblKifuLoadLast=\u68CB\u8B5C\u3092\u30ED\u30FC\u30C9\u3057\u305F\u6642\u306B\u6700\u7D42\u5C40\u9762\u3092\u8868\u793A LizzieConfig.lblAutoQuickAnalyzeOnLoad=\u68CB\u8B5C\u30ED\u30FC\u30C9\u5F8C\u306B\u52DD\u7387\u30B0\u30E9\u30D5\u3092\u3059\u3070\u3084\u304F\u66F4\u65B0 -LizzieConfig.lblTrackingMaxVisits=\u8FFD\u8DE1\u5206\u6790\u6700\u5927\u8A2A\u554F\u6570 +LizzieConfig.lblTrackingMaxVisits=move focus の累積訪問数目標 LizzieConfig.lblEnableDragStone=\u7881\u77F3\u3092\u30C9\u30E9\u30C3\u30B0 LizzieConfig.lblEnableClickReview=\u30AF\u30EA\u30C3\u30AF\u3067\u5909\u5316\u56F3\u3092\u78BA\u8A8D LizzieConfig.visits=\u56DE @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=\u5206\u6790\u9664\u5916 RightClickMenu.avoid2=\u96C6\u4E2D/\u9664\u5916\u3092\u7DAD\u6301\u3059\u308B RightClickMenu.previousMove=\u4E00\u624B\u623B\u308B RightClickMenu.cancelavoid=\u96C6\u4E2D/\u9664\u5916\u3092\u53D6\u308A\u6D88\u3059 -RightClickMenu.trackPoint=この点を追跡分析 -RightClickMenu.untrackPoint=追跡を解除 -RightClickMenu.clearAllTracked=全ての追跡をクリア +RightClickMenu.trackPoint=この点を評価 +RightClickMenu.trackPoint.tooltip=候補が累積訪問数の目標に達するまで、現在局面の通常解析を更新します +RightClickMenu.trackPoint.unavailableTooltip=安定した局面と、move focus 対応の現在のフォアグラウンドローカル KataGo が必要です +RightClickMenu.trackPoint.requiresCapability=move focus は利用不可(ローカル KataGo の対応が必要) +RightClickMenu.untrackPoint=評価をキャンセル +RightClickMenu.clearAllTracked=すべての評価点をクリア RightClickMenu.cleanedittemp=\u7DE8\u96C6\u30AD\u30E3\u30C3\u30B7\u30E5\u3092\u30AF\u30EA\u30A2 RightClickMenu.cleanupedit=\u5143\u306B\u623B\u3059 RightClickMenu.clearPriority=\u512A\u5148\u5EA6\u3092\u30AF\u30EA\u30A2 @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=単一曲線モードで面積塗りつ ConfigDialog2.modern.analysis.graphFillSub=表示中の曲線が1本のときだけ、50% / 0 の中線まで塗りつぶします ConfigDialog2.modern.analysis.maxRed=最も高い候補値を赤色で強調表示します。 ConfigDialog2.modern.analysis.maxRedSub=無効にすると、候補マーカーの値には通常の黒または白のテキストが使用されます。 -ConfigDialog2.modern.analysis.trackingVisits=追跡候補の訪問数 -ConfigDialog2.modern.analysis.trackingVisitsSub=各追跡候補がこの訪問数に達したら解析を停止します +ConfigDialog2.modern.analysis.trackingVisits=move focus の累積訪問数目標 +ConfigDialog2.modern.analysis.trackingVisitsSub=各候補がこの累積訪問数に達するまで通常解析に focus を追加します。通常解析の制限は変わりません ConfigDialog2.modern.unit.visits=回 ConfigDialog2.modern.analysis.suggestionLimit=候補の最大移動数 ConfigDialog2.modern.analysis.suggestionLimitSub=メインボードに表示される候補手を制限する @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=現在の思考がこの秒数に達 ConfigDialog2.modern.unit.seconds=秒 ConfigDialog2.modern.analysis.limitVisits=訪問数を制限 ConfigDialog2.modern.analysis.limitVisitsSub=現在の局面がこの訪問数に達すると一時停止します。オフで無制限です。 -ConfigDialog2.modern.trackingAppearance.title=追跡候補の外観 -ConfigDialog2.modern.trackingAppearance.subtitle=追跡結果の内側、動的な品質アウトライン、文字色を調整します。 -ConfigDialog2.modern.trackingAppearance.outline=追跡候補のアウトラインを表示 -ConfigDialog2.modern.trackingAppearance.outlineSub=待機状態とリアルタイムの手の品質を破線アウトラインで表示します -ConfigDialog2.modern.trackingAppearance.interiorColor=内側の色 -ConfigDialog2.modern.trackingAppearance.interiorOpacity=内側の不透明度 -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=固定の内側色の不透明度を調整します +ConfigDialog2.modern.trackingAppearance.title=注目点の品質アウトライン +ConfigDialog2.modern.trackingAppearance.subtitle=品質は現在の通常候補と order 1 候補の比較で更新され、活動状態と進捗は別に表示されます。 +ConfigDialog2.modern.trackingAppearance.outline=注目点の品質アウトラインを表示 +ConfigDialog2.modern.trackingAppearance.outlineSub=品質アウトラインだけを制御し、注目や解析は変更しません ConfigDialog2.modern.trackingAppearance.outlineOpacity=アウトラインの不透明度 ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=動的な品質アウトラインの不透明度を調整します -ConfigDialog2.modern.trackingAppearance.autoTextColor=文字色を自動調整 -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=内側の色、不透明度、盤面背景に応じて黒または白を選択します -ConfigDialog2.modern.trackingAppearance.textColor=文字色 -ConfigDialog2.modern.trackingAppearance.textColorSub=自動調整をオフにした場合に使用します ConfigDialog2.modern.play.title=再生とレビューのコントロール ConfigDialog2.modern.play.subtitle=ボード マーカー、ポインター アクションを構成し、インタラクションを確認します。 ConfigDialog2.modern.play.doubleClick=double-click 移動ルックアップを有効にする diff --git a/src/main/resources/l10n/DisplayStrings_ko.properties b/src/main/resources/l10n/DisplayStrings_ko.properties index 96b2c2bc6..37cffef36 100644 --- a/src/main/resources/l10n/DisplayStrings_ko.properties +++ b/src/main/resources/l10n/DisplayStrings_ko.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=\uC774\uC804\uC218\uC21C LizzieConfig.lblLoadKomi=\uAE30\uBCF4\uC5D0\uC11C \uB364 \uAC00\uC838\uC624\uAE30 LizzieConfig.lblKifuLoadLast=\uAE30\uBCF4 \uB85C\uB529\uC2DC \uB9C8\uC9C0\uB9C9\uC73C\uB85C \uC774\uB3D9 LizzieConfig.lblAutoQuickAnalyzeOnLoad=\uAE30\uBCF4 \uB85C\uB529 \uD6C4 \uC2B9\uB960 \uADF8\uB798\uD504 \uBE60\uB978 \uAC31\uC2E0 -LizzieConfig.lblTrackingMaxVisits=\uCD94\uC801 \uBD84\uC11D \uCD5C\uB300 \uBC29\uBB38\uC218 +LizzieConfig.lblTrackingMaxVisits=move focus 누적 방문 목표 LizzieConfig.lblEnableDragStone=\uB3CC \uB04C\uC5B4 \uC62E\uAE30\uAE30 LizzieConfig.lblEnableClickReview=\uB3CC \uD074\uB9AD\uD574\uC11C \uBD84\uC11D LizzieConfig.visits=뷔 @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=\uC81C\uC678 RightClickMenu.avoid2=\uD5C8\uC6A9/\uC81C\uC678 \uC720\uC9C0 RightClickMenu.previousMove=\uC774\uC804 \uC218\uC21C RightClickMenu.cancelavoid=\uD5C8\uC6A9/\uC81C\uC678 \uCDE8\uC18C -RightClickMenu.trackPoint=이 점 추적 분석 -RightClickMenu.untrackPoint=추적 해제 -RightClickMenu.clearAllTracked=모든 추적 지우기 +RightClickMenu.trackPoint=이 수 평가 +RightClickMenu.trackPoint.tooltip=후보가 누적 방문 목표에 도달할 때까지 현재 국면의 일반 분석을 갱신합니다 +RightClickMenu.trackPoint.unavailableTooltip=안정된 국면과 move focus를 지원하는 현재 포그라운드 로컬 KataGo가 필요합니다 +RightClickMenu.trackPoint.requiresCapability=move focus 사용 불가 (로컬 KataGo 지원 필요) +RightClickMenu.untrackPoint=평가 취소 +RightClickMenu.clearAllTracked=모든 평가 지점 지우기 RightClickMenu.cleanedittemp=Clean edit temp RightClickMenu.cleanupedit=실행 취소 RightClickMenu.clearPriority=우선순위 지우기 @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=단일 곡선 모드에서 영역 채우 ConfigDialog2.modern.analysis.graphFillSub=표시된 곡선이 하나일 때만 50% / 0 중선까지 채웁니다 ConfigDialog2.modern.analysis.maxRed=가장 높은 후보 값을 빨간색으로 강조 표시 ConfigDialog2.modern.analysis.maxRedSub=비활성화되면 후보 마커의 값이 일반 검정색 또는 흰색 텍스트를 사용합니다. -ConfigDialog2.modern.analysis.trackingVisits=추적 지점 방문 수 -ConfigDialog2.modern.analysis.trackingVisitsSub=각 추적 지점이 이 방문 수에 도달하면 분석을 중지합니다 +ConfigDialog2.modern.analysis.trackingVisits=move focus 누적 방문 목표 +ConfigDialog2.modern.analysis.trackingVisitsSub=각 후보가 이 누적 방문 수에 도달할 때까지 일반 분석에 focus를 추가하며 일반 분석 제한은 바뀌지 않습니다 ConfigDialog2.modern.unit.visits=회 ConfigDialog2.modern.analysis.suggestionLimit=최대 후보자 이동 ConfigDialog2.modern.analysis.suggestionLimitSub=메인보드에 표시된 후보 동작을 제한하세요. @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=현재 사고가 이 초 수에 도 ConfigDialog2.modern.unit.seconds=초 ConfigDialog2.modern.analysis.limitVisits=방문 수 제한 ConfigDialog2.modern.analysis.limitVisitsSub=현재 국면이 이 방문 수에 도달하면 일시 중지합니다. 해제하면 무제한입니다. -ConfigDialog2.modern.trackingAppearance.title=추적 후보 모양 -ConfigDialog2.modern.trackingAppearance.subtitle=추적 결과 내부, 실시간 품질 윤곽선 및 텍스트 색상을 조정합니다. -ConfigDialog2.modern.trackingAppearance.outline=추적점 윤곽선 표시 -ConfigDialog2.modern.trackingAppearance.outlineSub=대기 상태와 실시간 수 품질을 점선 윤곽선으로 표시합니다 -ConfigDialog2.modern.trackingAppearance.interiorColor=내부 색상 -ConfigDialog2.modern.trackingAppearance.interiorOpacity=내부 불투명도 -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=고정 내부 색상의 불투명도를 조정합니다 +ConfigDialog2.modern.trackingAppearance.title=관심 수 품질 윤곽선 +ConfigDialog2.modern.trackingAppearance.subtitle=품질은 현재 일반 후보와 order 1 후보의 비교에 따라 갱신되며 활동 상태와 진행률은 별도로 표시됩니다. +ConfigDialog2.modern.trackingAppearance.outline=관심 수 품질 윤곽선 표시 +ConfigDialog2.modern.trackingAppearance.outlineSub=품질 윤곽선만 제어하며 관심이나 분석은 변경하지 않습니다 ConfigDialog2.modern.trackingAppearance.outlineOpacity=윤곽선 불투명도 -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=실시간 품질 점선 윤곽선의 불투명도를 조정합니다 -ConfigDialog2.modern.trackingAppearance.autoTextColor=텍스트 색상 자동 조정 -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=내부 색상, 불투명도 및 바둑판 배경에 따라 검정 또는 흰색을 선택합니다 -ConfigDialog2.modern.trackingAppearance.textColor=텍스트 색상 -ConfigDialog2.modern.trackingAppearance.textColorSub=자동 조정을 끈 경우 사용합니다 +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=동적 품질 윤곽선의 불투명도를 조정합니다 ConfigDialog2.modern.play.title=재생 및 리뷰 제어 ConfigDialog2.modern.play.subtitle=보드 마커, 포인터 작업 및 검토 상호 작용을 구성합니다. ConfigDialog2.modern.play.doubleClick=double-click 이동 조회 활성화 diff --git a/src/main/resources/l10n/DisplayStrings_th_TH.properties b/src/main/resources/l10n/DisplayStrings_th_TH.properties index bca5810e3..421b0e54f 100644 --- a/src/main/resources/l10n/DisplayStrings_th_TH.properties +++ b/src/main/resources/l10n/DisplayStrings_th_TH.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=ก่อนหน้า LizzieConfig.lblLoadKomi=โหลดโคมิจาก SGF LizzieConfig.lblKifuLoadLast=ข้ามไปยังจุดสิ้นสุดหลังจากโหลดเกม LizzieConfig.lblAutoQuickAnalyzeOnLoad=กราฟอัตราการชนะอย่างรวดเร็วหลังโหลด -LizzieConfig.lblTrackingMaxVisits=ติดตามและวิเคราะห์จำนวนการเข้าชมสูงสุด +LizzieConfig.lblTrackingMaxVisits=เป้าหมายจำนวนครั้งสะสมของ move focus LizzieConfig.lblEnableDragStone=ลากสโตน LizzieConfig.lblEnableClickReview=คลิกรีวิวหิน LizzieConfig.visits=วี @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=หลีกเลี่ยง RightClickMenu.avoid2=เก็บไว้ในอนุญาต/หลีกเลี่ยง RightClickMenu.previousMove=ย้ายก่อนหน้า RightClickMenu.cancelavoid=ยกเลิก หลีกเลี่ยง/อนุญาต -RightClickMenu.trackPoint=ติดตามจุดนี้ -RightClickMenu.untrackPoint=ยกเลิกการติดตามจุดนี้ -RightClickMenu.clearAllTracked=ล้างการติดตามทั้งหมด +RightClickMenu.trackPoint=ประเมินจุดนี้ +RightClickMenu.trackPoint.tooltip=อัปเดตการวิเคราะห์ปกติของตำแหน่งนี้จนกว่าตัวเลือกจะถึงเป้าหมายจำนวนครั้งสะสม +RightClickMenu.trackPoint.unavailableTooltip=ต้องมีตำแหน่งที่เสถียรและ KataGo ภายในเครื่องที่อยู่เบื้องหน้าและรองรับ move focus +RightClickMenu.trackPoint.requiresCapability=ใช้ move focus ไม่ได้ (ต้องใช้ KataGo ภายในเครื่องที่รองรับ) +RightClickMenu.untrackPoint=ยกเลิกการประเมิน +RightClickMenu.clearAllTracked=ล้างจุดประเมินทั้งหมด RightClickMenu.cleanedittemp=ล้างแคชแก้ไข RightClickMenu.cleanupedit=เลิกทำ RightClickMenu.clearPriority=ลำดับความสำคัญที่ชัดเจน @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=ใช้การเติมพื้ ConfigDialog2.modern.analysis.graphFillSub=เติมจากเส้นโค้งที่มองเห็นเส้นเดียวไปยังเส้นกลาง 50% / 0 ConfigDialog2.modern.analysis.maxRed=ไฮไลต์ค่าตัวเลือกสูงสุดด้วยสีแดง ConfigDialog2.modern.analysis.maxRedSub=เมื่อปิดใช้งาน ค่าบนเครื่องหมายตัวเลือกจะใช้ข้อความสีดำหรือสีขาวปกติ -ConfigDialog2.modern.analysis.trackingVisits=จำนวนการเข้าชมจุดติดตาม -ConfigDialog2.modern.analysis.trackingVisitsSub=หยุดวิเคราะห์แต่ละจุดติดตามเมื่อถึงจำนวนการเข้าชมนี้ +ConfigDialog2.modern.analysis.trackingVisits=เป้าหมายจำนวนครั้งสะสมของ move focus +ConfigDialog2.modern.analysis.trackingVisitsSub=เพิ่ม focus ในการวิเคราะห์ปกติจนแต่ละตัวเลือกถึงจำนวนครั้งสะสมนี้ โดยไม่เปลี่ยนขีดจำกัดการวิเคราะห์ปกติ ConfigDialog2.modern.unit.visits=ครั้ง ConfigDialog2.modern.analysis.suggestionLimit=การย้ายผู้สมัครสูงสุด ConfigDialog2.modern.analysis.suggestionLimitSub=จำกัดการเคลื่อนไหวของผู้สมัครที่แสดงบนกระดานหลัก @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=หยุดการวิเคร ConfigDialog2.modern.unit.seconds=วินาที ConfigDialog2.modern.analysis.limitVisits=จำกัดจำนวนวิสิต ConfigDialog2.modern.analysis.limitVisitsSub=หยุดการวิเคราะห์สดเมื่อถึงจำนวนวิสิตนี้ ยกเลิกคือไม่จำกัด -ConfigDialog2.modern.trackingAppearance.title=ลักษณะจุดติดตาม -ConfigDialog2.modern.trackingAppearance.subtitle=ปรับสีภายในผลการติดตาม เส้นขอบคุณภาพแบบไดนามิก และสีข้อความ -ConfigDialog2.modern.trackingAppearance.outline=แสดงเส้นขอบจุดติดตาม -ConfigDialog2.modern.trackingAppearance.outlineSub=ใช้เส้นขอบแบบประเพื่อแสดงสถานะรอและคุณภาพหมากแบบเรียลไทม์ -ConfigDialog2.modern.trackingAppearance.interiorColor=สีภายใน -ConfigDialog2.modern.trackingAppearance.interiorOpacity=ความทึบภายใน -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=ปรับความทึบของสีภายในแบบคงที่ +ConfigDialog2.modern.trackingAppearance.title=เส้นขอบคุณภาพของจุดสนใจ +ConfigDialog2.modern.trackingAppearance.subtitle=คุณภาพอัปเดตจากตัวเลือกปกติปัจจุบันเทียบกับตัวเลือก order 1 ส่วนสถานะทำงานและความคืบหน้าแสดงแยกกัน +ConfigDialog2.modern.trackingAppearance.outline=แสดงเส้นขอบคุณภาพของจุดสนใจ +ConfigDialog2.modern.trackingAppearance.outlineSub=ควบคุมเฉพาะเส้นขอบคุณภาพ โดยไม่ล้างจุดสนใจหรือเปลี่ยนการวิเคราะห์ ConfigDialog2.modern.trackingAppearance.outlineOpacity=ความทึบของเส้นขอบ ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=ปรับความทึบของเส้นขอบคุณภาพแบบไดนามิก -ConfigDialog2.modern.trackingAppearance.autoTextColor=ปรับสีข้อความอัตโนมัติ -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=เลือกสีดำหรือสีขาวตามสีภายใน ความทึบ และพื้นหลังกระดาน -ConfigDialog2.modern.trackingAppearance.textColor=สีข้อความ -ConfigDialog2.modern.trackingAppearance.textColorSub=ใช้เมื่อปิดการปรับสีข้อความอัตโนมัติ ConfigDialog2.modern.play.title=การควบคุมการเล่นและทบทวน ConfigDialog2.modern.play.subtitle=กำหนดค่ามาร์กเกอร์บนบอร์ด การดำเนินการของพอยน์เตอร์ และตรวจสอบการโต้ตอบ ConfigDialog2.modern.play.doubleClick=เปิดใช้งานการค้นหาการย้าย double-click diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index aef2a3c56..251ab3fdf 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=\u56DE\u9000\u4E00\u624B LizzieConfig.lblLoadKomi=\u52A0\u8F7D\u68CB\u8C31\u4E2D\u7684\u8D34\u76EE LizzieConfig.lblKifuLoadLast=\u52A0\u8F7D\u68CB\u8C31\u540E\u8DF3\u8F6C\u5230\u6700\u540E LizzieConfig.lblAutoQuickAnalyzeOnLoad=\u52A0\u8F7D\u68CB\u8C31\u540E\u5FEB\u901F\u5237\u65B0\u80DC\u7387\u66F2\u7EBF -LizzieConfig.lblTrackingMaxVisits=\u9009\u70B9\u8BC4\u4F30\u8BA1\u7B97\u91CF +LizzieConfig.lblTrackingMaxVisits=选点重点研究累计计算量目标 LizzieConfig.lblEnableDragStone=\u542F\u7528\u62D6\u62FD\u68CB\u5B50 LizzieConfig.lblEnableClickReview=\u70B9\u51FB\u68CB\u5B50\u56DE\u770B LizzieConfig.visits=\u8BA1\u7B97\u91CF @@ -2370,8 +2370,11 @@ RightClickMenu.avoid2=\u6301\u7EED\u5206\u6790/\u4E0D\u5206\u6790 RightClickMenu.previousMove=\u56DE\u9000\u4E00\u624B RightClickMenu.cancelavoid=\u6E05\u9664\u5206\u6790/\u4E0D\u5206\u6790 RightClickMenu.trackPoint=评估此点 -RightClickMenu.untrackPoint=取消评估此点 -RightClickMenu.clearAllTracked=清除所有评估结果 +RightClickMenu.trackPoint.tooltip=更新当前局面的普通分析,直到此候选达到累计计算量目标 +RightClickMenu.trackPoint.unavailableTooltip=需要稳定局面,以及当前前台本地 KataGo 已支持选点重点研究 +RightClickMenu.trackPoint.requiresCapability=选点重点研究不可用(需要本地 KataGo 支持) +RightClickMenu.untrackPoint=取消评估 +RightClickMenu.clearAllTracked=清除所有评估选点 RightClickMenu.cleanedittemp=\u6E05\u9664\u7F16\u8F91\u7F13\u5B58 RightClickMenu.cleanupedit=\u64A4\u9500\u7F16\u8F91 RightClickMenu.clearPriority=\u6E05\u9664\u4F18\u5148\u5206\u6790 @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=单曲线模式使用面积填充 ConfigDialog2.modern.analysis.graphFillSub=仅当图上只有一条曲线时,填充到 50% / 0 中线 ConfigDialog2.modern.analysis.maxRed=候选点最高值红色高亮 ConfigDialog2.modern.analysis.maxRedSub=关闭后蓝点上的胜率数字使用普通黑白文字,不再反成红色 -ConfigDialog2.modern.analysis.trackingVisits=选点评估计算量 -ConfigDialog2.modern.analysis.trackingVisitsSub=每个选点达到该计算量后停止评估 +ConfigDialog2.modern.analysis.trackingVisits=选点重点研究累计计算量目标 +ConfigDialog2.modern.analysis.trackingVisitsSub=在普通分析中增加重点研究,直到每个候选达到此累计计算量;普通分析限制不变 ConfigDialog2.modern.unit.visits=次 ConfigDialog2.modern.analysis.suggestionLimit=选点数量上限 ConfigDialog2.modern.analysis.suggestionLimitSub=限制主界面推荐选点数量 @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=当前思考达到该秒数后暂停 ConfigDialog2.modern.unit.seconds=秒 ConfigDialog2.modern.analysis.limitVisits=限制分析计算量 ConfigDialog2.modern.analysis.limitVisitsSub=当前局面达到该计算量后暂停。取消勾选表示不限制。 -ConfigDialog2.modern.trackingAppearance.title=选点评估结果外观 -ConfigDialog2.modern.trackingAppearance.subtitle=调整评估结果的底色、动态质量外框和文字颜色。 -ConfigDialog2.modern.trackingAppearance.outline=显示评估结果外框 -ConfigDialog2.modern.trackingAppearance.outlineSub=用虚线外框显示等待状态和实时落子质量 -ConfigDialog2.modern.trackingAppearance.interiorColor=内部颜色 -ConfigDialog2.modern.trackingAppearance.interiorOpacity=内部不透明度 -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=调整固定内部颜色的不透明程度 +ConfigDialog2.modern.trackingAppearance.title=关注点质量外框 +ConfigDialog2.modern.trackingAppearance.subtitle=质量依据当前普通候选与第 1 顺位候选动态更新;活动状态和进度另行显示。 +ConfigDialog2.modern.trackingAppearance.outline=显示关注点质量外框 +ConfigDialog2.modern.trackingAppearance.outlineSub=只控制质量外框,不清除关注,也不改变分析 ConfigDialog2.modern.trackingAppearance.outlineOpacity=外框不透明度 -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=调整动态质量虚线外框的不透明程度 -ConfigDialog2.modern.trackingAppearance.autoTextColor=文字颜色自动适配 -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=根据内部颜色、不透明度和棋盘背景自动选择黑字或白字 -ConfigDialog2.modern.trackingAppearance.textColor=文字颜色 -ConfigDialog2.modern.trackingAppearance.textColorSub=关闭自动适配后使用此颜色 +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=调整动态质量外框的不透明程度 ConfigDialog2.modern.play.title=对局与操作 ConfigDialog2.modern.play.subtitle=整理棋盘标记、鼠标操作和复盘交互选项。 ConfigDialog2.modern.play.doubleClick=启用双击找子 diff --git a/src/main/resources/l10n/DisplayStrings_zh_HK.properties b/src/main/resources/l10n/DisplayStrings_zh_HK.properties index 21c4ba677..8e4f8297b 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_HK.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_HK.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=\u56DE\u9000\u4E00\u624B LizzieConfig.lblLoadKomi=\u52A0\u8F09\u68CB\u8B5C\u4E2D\u7684\u8CBC\u76EE LizzieConfig.lblKifuLoadLast=\u52A0\u8F09\u68CB\u8B5C\u5F8C\u8DF3\u8F49\u5230\u6700\u5F8C LizzieConfig.lblAutoQuickAnalyzeOnLoad=\u52A0\u8F09\u68CB\u8B5C\u5F8C\u5FEB\u901F\u5237\u65B0\u52DD\u7387\u66F2\u7DDA -LizzieConfig.lblTrackingMaxVisits=\u9078\u9EDE\u8A55\u4F30\u8A08\u7B97\u91CF +LizzieConfig.lblTrackingMaxVisits=選點重點研究累計計算量目標 LizzieConfig.lblEnableDragStone=\u5553\u7528\u62D6\u62FD\u68CB\u5B50 LizzieConfig.lblEnableClickReview=\u9EDE\u64CA\u68CB\u5B50\u56DE\u770B LizzieConfig.visits=\u8A08\u7B97\u91CF @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=\u4E0D\u5206\u6790\u6B64\u9EDE RightClickMenu.avoid2=\u6301\u7E8C\u5206\u6790/\u4E0D\u5206\u6790 RightClickMenu.previousMove=\u56DE\u9000\u4E00\u624B RightClickMenu.cancelavoid=\u6E05\u9664\u5206\u6790/\u4E0D\u5206\u6790 -RightClickMenu.trackPoint=\u8A55\u4F30\u6B64\u9EDE -RightClickMenu.untrackPoint=\u53D6\u6D88\u8A55\u4F30\u6B64\u9EDE -RightClickMenu.clearAllTracked=\u6E05\u9664\u6240\u6709\u8A55\u4F30\u7D50\u679C +RightClickMenu.trackPoint=評估此點 +RightClickMenu.trackPoint.tooltip=更新目前局面的普通分析,直到此候選達到累計計算量目標 +RightClickMenu.trackPoint.unavailableTooltip=需要穩定局面,以及目前前台本地 KataGo 已支援選點重點研究 +RightClickMenu.trackPoint.requiresCapability=選點重點研究不可用(需要本地 KataGo 支援) +RightClickMenu.untrackPoint=取消評估 +RightClickMenu.clearAllTracked=清除所有評估選點 RightClickMenu.cleanedittemp=\u6E05\u9664\u7DE8\u8F2F\u7DE9\u5B58 RightClickMenu.cleanupedit=\u64A4\u92B7\u7DE8\u8F2F RightClickMenu.clearPriority=\u6E05\u9664\u512A\u5148\u5206\u6790 @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=單曲線模式使用面積填充 ConfigDialog2.modern.analysis.graphFillSub=僅當圖上只有一條曲線時,填充到 50% / 0 中線 ConfigDialog2.modern.analysis.maxRed=\u7528\u7D05\u8272\u7A81\u51FA\u986F\u793A\u6700\u9AD8\u5019\u9078\u503C ConfigDialog2.modern.analysis.maxRedSub=\u505C\u7528\u6642\uFF0C\u5019\u9078\u6A19\u8A18\u4E0A\u7684\u503C\u4F7F\u7528\u5E38\u898F\u9ED1\u8272\u6216\u767D\u8272\u6587\u5B57 -ConfigDialog2.modern.analysis.trackingVisits=選點評估計算量 -ConfigDialog2.modern.analysis.trackingVisitsSub=每個選點達到此計算量後停止評估 +ConfigDialog2.modern.analysis.trackingVisits=選點重點研究累計計算量目標 +ConfigDialog2.modern.analysis.trackingVisitsSub=在普通分析中增加重點研究,直到每個候選達到此累計計算量;普通分析限制不變 ConfigDialog2.modern.unit.visits=次 ConfigDialog2.modern.analysis.suggestionLimit=\u6700\u5927\u5019\u9078\u4EBA\u79FB\u52D5 ConfigDialog2.modern.analysis.suggestionLimitSub=\u9650\u5236\u4E3B\u6A5F\u677F\u4E0A\u986F\u793A\u7684\u5019\u9078\u52D5\u4F5C @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=目前思考達到該秒數後暫停 ConfigDialog2.modern.unit.seconds=秒 ConfigDialog2.modern.analysis.limitVisits=限制分析計算量 ConfigDialog2.modern.analysis.limitVisitsSub=目前局面達到該計算量後暫停。取消勾選表示不限制。 -ConfigDialog2.modern.trackingAppearance.title=選點評估結果外觀 -ConfigDialog2.modern.trackingAppearance.subtitle=調整評估結果的底色、動態品質外框和文字顏色。 -ConfigDialog2.modern.trackingAppearance.outline=顯示評估結果外框 -ConfigDialog2.modern.trackingAppearance.outlineSub=用虛線外框顯示等待狀態和即時落子品質 -ConfigDialog2.modern.trackingAppearance.interiorColor=內部顏色 -ConfigDialog2.modern.trackingAppearance.interiorOpacity=內部不透明度 -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=調整固定內部顏色的不透明程度 +ConfigDialog2.modern.trackingAppearance.title=關注點品質外框 +ConfigDialog2.modern.trackingAppearance.subtitle=品質依據目前普通候選與第 1 順位候選動態更新;活動狀態和進度另行顯示。 +ConfigDialog2.modern.trackingAppearance.outline=顯示關注點品質外框 +ConfigDialog2.modern.trackingAppearance.outlineSub=只控制品質外框,不清除關注,也不改變分析 ConfigDialog2.modern.trackingAppearance.outlineOpacity=外框不透明度 -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=調整動態品質虛線外框的不透明程度 -ConfigDialog2.modern.trackingAppearance.autoTextColor=文字顏色自動調整 -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=根據內部顏色、不透明度和棋盤背景自動選擇黑字或白字 -ConfigDialog2.modern.trackingAppearance.textColor=文字顏色 -ConfigDialog2.modern.trackingAppearance.textColorSub=關閉自動調整後使用此顏色 +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=調整動態品質外框的不透明程度 ConfigDialog2.modern.play.title=\u64AD\u653E\u548C\u67E5\u770B\u63A7\u4EF6 ConfigDialog2.modern.play.subtitle=\u914D\u7F6E\u677F\u6A19\u8A18\u3001\u6307\u91DD\u64CD\u4F5C\u4E26\u67E5\u770B\u4E92\u52D5\u3002 ConfigDialog2.modern.play.doubleClick=\u555F\u7528 double-click \u79FB\u52D5\u67E5\u627E diff --git a/src/main/resources/l10n/DisplayStrings_zh_TW.properties b/src/main/resources/l10n/DisplayStrings_zh_TW.properties index 68c36c933..f57c5cfcd 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_TW.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_TW.properties @@ -1424,7 +1424,7 @@ LizzieConfig.rdoRightClickBack=\u56DE\u9000\u4E00\u624B LizzieConfig.lblLoadKomi=\u8F09\u5165\u68CB\u8B5C\u4E2D\u7684\u8CBC\u76EE LizzieConfig.lblKifuLoadLast=\u8F09\u5165\u68CB\u8B5C\u5F8C\u8DF3\u8F49\u5230\u6700\u5F8C LizzieConfig.lblAutoQuickAnalyzeOnLoad=\u8F09\u5165\u68CB\u8B5C\u5F8C\u5FEB\u901F\u91CD\u65B0\u6574\u7406\u52DD\u7387\u66F2\u7DDA -LizzieConfig.lblTrackingMaxVisits=\u9078\u9EDE\u8A55\u4F30\u8A08\u7B97\u91CF +LizzieConfig.lblTrackingMaxVisits=選點重點研究累計計算量目標 LizzieConfig.lblEnableDragStone=\u555F\u7528\u62D6\u62FD\u68CB\u5B50 LizzieConfig.lblEnableClickReview=\u9EDE\u9078\u68CB\u5B50\u56DE\u770B LizzieConfig.visits=\u8A08\u7B97\u91CF @@ -2369,9 +2369,12 @@ RightClickMenu.avoid=\u4E0D\u5206\u6790\u6B64\u9EDE RightClickMenu.avoid2=\u6301\u7E8C\u5206\u6790/\u4E0D\u5206\u6790 RightClickMenu.previousMove=\u56DE\u9000\u4E00\u624B RightClickMenu.cancelavoid=\u6E05\u9664\u5206\u6790/\u4E0D\u5206\u6790 -RightClickMenu.trackPoint=\u8A55\u4F30\u6B64\u9EDE -RightClickMenu.untrackPoint=\u53D6\u6D88\u8A55\u4F30\u6B64\u9EDE -RightClickMenu.clearAllTracked=\u6E05\u9664\u6240\u6709\u8A55\u4F30\u7D50\u679C +RightClickMenu.trackPoint=評估此點 +RightClickMenu.trackPoint.tooltip=更新目前局面的普通分析,直到此候選達到累計計算量目標 +RightClickMenu.trackPoint.unavailableTooltip=需要穩定局面,以及目前前台本地 KataGo 已支援選點重點研究 +RightClickMenu.trackPoint.requiresCapability=選點重點研究不可用(需要本地 KataGo 支援) +RightClickMenu.untrackPoint=取消評估 +RightClickMenu.clearAllTracked=清除所有評估選點 RightClickMenu.cleanedittemp=\u6E05\u9664\u7DE8\u8F2F\u5FEB\u53D6 RightClickMenu.cleanupedit=\u64A4\u92B7\u7DE8\u8F2F RightClickMenu.clearPriority=\u6E05\u9664\u512A\u5148\u5206\u6790 @@ -3164,8 +3167,8 @@ ConfigDialog2.modern.analysis.graphFill=單曲線模式使用面積填充 ConfigDialog2.modern.analysis.graphFillSub=僅當圖上只有一條曲線時,填充到 50% / 0 中線 ConfigDialog2.modern.analysis.maxRed=\u7528\u7D05\u8272\u7A81\u51FA\u986F\u793A\u6700\u9AD8\u5019\u9078\u503C ConfigDialog2.modern.analysis.maxRedSub=\u505C\u7528\u6642\uFF0C\u5019\u9078\u6A19\u8A18\u4E0A\u7684\u503C\u4F7F\u7528\u5E38\u898F\u9ED1\u8272\u6216\u767D\u8272\u6587\u5B57 -ConfigDialog2.modern.analysis.trackingVisits=選點評估計算量 -ConfigDialog2.modern.analysis.trackingVisitsSub=每個選點達到此計算量後停止評估 +ConfigDialog2.modern.analysis.trackingVisits=選點重點研究累計計算量目標 +ConfigDialog2.modern.analysis.trackingVisitsSub=在普通分析中增加重點研究,直到每個候選達到此累計計算量;普通分析限制不變 ConfigDialog2.modern.unit.visits=次 ConfigDialog2.modern.analysis.suggestionLimit=\u6700\u5927\u5019\u9078\u4EBA\u79FB\u52D5 ConfigDialog2.modern.analysis.suggestionLimitSub=\u9650\u5236\u4E3B\u6A5F\u677F\u4E0A\u986F\u793A\u7684\u5019\u9078\u52D5\u4F5C @@ -3180,19 +3183,12 @@ ConfigDialog2.modern.analysis.limitTimeSub=目前思考達到該秒數後暫停 ConfigDialog2.modern.unit.seconds=秒 ConfigDialog2.modern.analysis.limitVisits=限制分析計算量 ConfigDialog2.modern.analysis.limitVisitsSub=目前局面達到該計算量後暫停。取消勾選表示不限制。 -ConfigDialog2.modern.trackingAppearance.title=選點評估結果外觀 -ConfigDialog2.modern.trackingAppearance.subtitle=調整評估結果的底色、動態品質外框和文字顏色。 -ConfigDialog2.modern.trackingAppearance.outline=顯示評估結果外框 -ConfigDialog2.modern.trackingAppearance.outlineSub=用虛線外框顯示等待狀態和即時落子品質 -ConfigDialog2.modern.trackingAppearance.interiorColor=內部顏色 -ConfigDialog2.modern.trackingAppearance.interiorOpacity=內部不透明度 -ConfigDialog2.modern.trackingAppearance.interiorOpacitySub=調整固定內部顏色的不透明程度 +ConfigDialog2.modern.trackingAppearance.title=關注點品質外框 +ConfigDialog2.modern.trackingAppearance.subtitle=品質依據目前普通候選與第 1 順位候選動態更新;活動狀態和進度另行顯示。 +ConfigDialog2.modern.trackingAppearance.outline=顯示關注點品質外框 +ConfigDialog2.modern.trackingAppearance.outlineSub=只控制品質外框,不清除關注,也不改變分析 ConfigDialog2.modern.trackingAppearance.outlineOpacity=外框不透明度 -ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=調整動態品質虛線外框的不透明程度 -ConfigDialog2.modern.trackingAppearance.autoTextColor=文字顏色自動調整 -ConfigDialog2.modern.trackingAppearance.autoTextColorSub=根據內部顏色、不透明度和棋盤背景自動選擇黑字或白字 -ConfigDialog2.modern.trackingAppearance.textColor=文字顏色 -ConfigDialog2.modern.trackingAppearance.textColorSub=關閉自動調整後使用此顏色 +ConfigDialog2.modern.trackingAppearance.outlineOpacitySub=調整動態品質外框的不透明程度 ConfigDialog2.modern.play.title=\u64AD\u653E\u548C\u6AA2\u8996\u63A7\u5236\u5143\u4EF6 ConfigDialog2.modern.play.subtitle=\u914D\u7F6E\u677F\u6A19\u8A18\u3001\u6307\u6A19\u64CD\u4F5C\u4E26\u6AA2\u8996\u4E92\u52D5\u3002 ConfigDialog2.modern.play.doubleClick=\u555F\u7528 double-click \u79FB\u52D5\u67E5\u8A62 diff --git a/src/test/java/featurecat/lizzie/TrackingConfigMigrationTest.java b/src/test/java/featurecat/lizzie/TrackingConfigMigrationTest.java index 6a40a0722..c2887ee19 100644 --- a/src/test/java/featurecat/lizzie/TrackingConfigMigrationTest.java +++ b/src/test/java/featurecat/lizzie/TrackingConfigMigrationTest.java @@ -4,71 +4,66 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.awt.Color; import java.nio.file.Files; -import java.nio.file.Path; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; class TrackingConfigMigrationTest { @Test - void trackingPointAppearanceUsesTheAgreedDefaultsWhenSettingsAreAbsent() throws Exception { + void trackingPointAppearanceKeepsOnlyAttentionOutlineDefaults() throws Exception { Config config = ConfigTestHelper.createForTests(Files.createTempDirectory("tracking-style")); + JSONObject ui = + new JSONObject() + .put("tracking-point-interior-color", new JSONArray().put(10).put(20).put(30)) + .put("tracking-point-interior-opacity", 37) + .put("tracking-point-text-auto-color", false) + .put("tracking-point-text-color", new JSONArray().put(210).put(220).put(230)); - config.loadTrackingPointAppearanceConfig(new JSONObject()); + config.loadTrackingPointAppearanceConfig(ui); assertTrue(config.showTrackingPointOutline); - assertEquals(new Color(255, 156, 156), config.trackingPointInteriorColor); - assertEquals(100, config.trackingPointInteriorOpacityPercent); assertEquals(92, config.trackingPointOutlineOpacityPercent); - assertTrue(config.trackingPointTextAutoColor); - assertEquals(Color.BLACK, config.trackingPointTextColor); + assertFalse(ui.has("tracking-point-interior-color")); + assertFalse(ui.has("tracking-point-interior-opacity")); + assertFalse(ui.has("tracking-point-text-auto-color")); + assertFalse(ui.has("tracking-point-text-color")); } @Test - void trackingPointAppearancePersistsEveryUserAdjustableValue() throws Exception { + void trackingPointAppearancePersistsOnlyAttentionOutlineSettings() throws Exception { Config config = ConfigTestHelper.createForTests(Files.createTempDirectory("tracking-style")); - config.uiConfig = new JSONObject(); + config.uiConfig = + new JSONObject() + .put("tracking-point-interior-color", new JSONArray()) + .put("tracking-point-interior-opacity", 37) + .put("tracking-point-text-auto-color", false) + .put("tracking-point-text-color", new JSONArray()); config.showTrackingPointOutline = false; - config.trackingPointInteriorColor = new Color(10, 20, 30); - config.trackingPointInteriorOpacityPercent = 37; config.trackingPointOutlineOpacityPercent = 64; - config.trackingPointTextAutoColor = false; - config.trackingPointTextColor = new Color(210, 220, 230); config.saveTrackingPointAppearanceConfig(); assertFalse(config.uiConfig.getBoolean("show-tracking-point-outline")); - assertEquals( - "[10,20,30]", config.uiConfig.getJSONArray("tracking-point-interior-color").toString()); - assertEquals(37, config.uiConfig.getInt("tracking-point-interior-opacity")); assertEquals(64, config.uiConfig.getInt("tracking-point-outline-opacity")); - assertFalse(config.uiConfig.getBoolean("tracking-point-text-auto-color")); - assertEquals( - "[210,220,230]", config.uiConfig.getJSONArray("tracking-point-text-color").toString()); + assertFalse(config.uiConfig.has("tracking-point-interior-color")); + assertFalse(config.uiConfig.has("tracking-point-interior-opacity")); + assertFalse(config.uiConfig.has("tracking-point-text-auto-color")); + assertFalse(config.uiConfig.has("tracking-point-text-color")); } @Test - void trackingPointAppearanceLoadsEveryUserAdjustableValue() throws Exception { + void trackingPointAppearanceLoadsAndClampsRetainedSettings() throws Exception { Config config = ConfigTestHelper.createForTests(Files.createTempDirectory("tracking-style")); JSONObject ui = new JSONObject() .put("show-tracking-point-outline", false) - .put("tracking-point-interior-color", new JSONArray().put(10).put(20).put(30)) - .put("tracking-point-interior-opacity", 37) - .put("tracking-point-outline-opacity", 64) - .put("tracking-point-text-auto-color", false) - .put("tracking-point-text-color", new JSONArray().put(210).put(220).put(230)); + .put("tracking-point-outline-opacity", 164); config.loadTrackingPointAppearanceConfig(ui); assertFalse(config.showTrackingPointOutline); - assertEquals(new Color(10, 20, 30), config.trackingPointInteriorColor); - assertEquals(37, config.trackingPointInteriorOpacityPercent); - assertEquals(64, config.trackingPointOutlineOpacityPercent); - assertFalse(config.trackingPointTextAutoColor); - assertEquals(new Color(210, 220, 230), config.trackingPointTextColor); + assertEquals(100, config.trackingPointOutlineOpacityPercent); } @Test @@ -100,84 +95,4 @@ void currentSingleStreamVisitsWinOverLegacyValue() { assertFalse(ui.has("tracking-engine-max-visits")); } - @Test - void changingEitherTrackingParameterAtTheProductionSettingsEntryClearsTheOldContext() - throws Exception { - String dialog = - Files.readString(Path.of("src/main/java/featurecat/lizzie/gui/ConfigDialog2.java")); - - assertTrue(dialog.contains("previousTrackingAnalysisMaxVisits")); - assertTrue(dialog.contains("previousAnalyzeUpdateIntervalCentisec")); - assertTrue(dialog.contains("Lizzie.config.analyzeUpdateIntervalCentisec")); - assertTrue(dialog.contains("!= previousAnalyzeUpdateIntervalCentisec")); - assertTrue(dialog.contains("Lizzie.frame.invalidateTrackingAnalysis()")); - assertFalse(dialog.contains("Lizzie.frame.clearTrackingPoints()")); - } - - @Test - void modernGeneralSettingsExposeTheTrackingVisitLimit() throws Exception { - String dialog = - Files.readString(Path.of("src/main/java/featurecat/lizzie/gui/ConfigDialog2.java")); - int engineSectionStart = dialog.indexOf("case MODERN_NAV_ENGINE:"); - int engineSectionEnd = dialog.indexOf("return content;", engineSectionStart); - - assertTrue(engineSectionStart >= 0); - assertTrue(engineSectionEnd > engineSectionStart); - assertTrue( - dialog - .substring(engineSectionStart, engineSectionEnd) - .contains("txtTrackingAnalysisMaxVisits"), - "综合设置的引擎与分析 section 应显示追踪选点计算量"); - } - - @Test - void modernEngineSettingsExposeLiveAnalysisDurationAndVisitLimits() throws Exception { - String dialog = - Files.readString(Path.of("src/main/java/featurecat/lizzie/gui/ConfigDialog2.java")); - int engineSectionStart = dialog.indexOf("case MODERN_NAV_ENGINE:"); - int engineSectionEnd = dialog.indexOf("return content;", engineSectionStart); - - assertTrue(engineSectionStart >= 0); - assertTrue(engineSectionEnd > engineSectionStart); - String engineSection = dialog.substring(engineSectionStart, engineSectionEnd); - assertTrue( - containsToggleInputRow(engineSection, "chkLimitTime", "txtMaxAnalyzeTime"), - "综合设置的引擎与分析 section 应显示实况分析时长限制"); - assertTrue( - containsToggleInputRow(engineSection, "chkLimitPlayouts", "txtLimitPlayouts"), - "综合设置的引擎与分析 section 应显示实况分析计算量限制"); - } - - private static boolean containsToggleInputRow(String section, String toggle, String field) { - int from = 0; - while (true) { - int call = section.indexOf("addToggleInputRow(", from); - if (call < 0) { - return false; - } - int open = section.indexOf('(', call); - int depth = 0; - int end = -1; - for (int i = open; i < section.length(); i++) { - char ch = section.charAt(i); - if (ch == '(') { - depth++; - } else if (ch == ')') { - depth--; - if (depth == 0) { - end = i; - break; - } - } - } - if (end < 0) { - return false; - } - String args = section.substring(open, end); - if (args.contains(toggle) && args.contains(field)) { - return true; - } - from = call + 1; - } - } } diff --git a/src/test/java/featurecat/lizzie/analysis/AnalysisEngineRequestTest.java b/src/test/java/featurecat/lizzie/analysis/AnalysisEngineRequestTest.java index e6e121f60..ea2ea87bf 100644 --- a/src/test/java/featurecat/lizzie/analysis/AnalysisEngineRequestTest.java +++ b/src/test/java/featurecat/lizzie/analysis/AnalysisEngineRequestTest.java @@ -25,12 +25,9 @@ import featurecat.lizzie.rules.Zobrist; import featurecat.lizzie.util.KataGoRuntimeHelper; import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.StringReader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; @@ -398,641 +395,7 @@ void terminatedSharedForegroundAnalysisEngineIsNotRunning() throws Exception { } } - @Test - void reuseModeClaimsAcquiringTrackingBeforeStartingTheForegroundRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - AnalysisEngine engine = new AnalysisEngine(false); - engine.startRequest(1, -1, false); - - assertTrue(engine.isLoaded(), "tracking should be a competitive foreground handoff"); - assertEquals("800000000 stop\n", output.toString(StandardCharsets.UTF_8)); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue( - output.toString(StandardCharsets.UTF_8).contains("830000000 kata-get-rules\n"), - "foreground request must activate only after the tracking fence"); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeClaimsActiveTrackingBeforeStartingTheForegroundRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue( - output.toString(StandardCharsets.UTF_8).contains("kata-get-rules\n"), - output.toString(StandardCharsets.UTF_8)); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeClaimsActiveTrackingBeforeStartingMissingMainlineRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, - foreground.previewForegroundAnalysisLeaseAvailability(), - "preview remains competitive so the business owner can create a typed handoff target"); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE, - foreground.beginForegroundAnalysisLease(new Object(), line -> {}, () -> {}, () -> {}), - "direct acquisition must not overwrite the active tracking session"); - int requested = engine.startRequestMissingMainline(false); - - assertEquals(1, requested); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue( - output.toString(StandardCharsets.UTF_8).contains("kata-get-rules\n"), - output.toString(StandardCharsets.UTF_8)); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void missingMainlineTrackingHandoffSurvivesBrowsingWithinTheSameKifu() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - assertEquals(1, engine.startRequestMissingMainline(false)); - assertTrue(Lizzie.board.getHistory().previous().isPresent()); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue( - output.toString(StandardCharsets.UTF_8).contains("kata-get-rules\n"), - "browsing the same kifu must not cancel pending quick-curve handoff"); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeClaimsActiveTrackingBeforeStartingWholeGameRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(foreground); - AnalysisEngine engine = - new AnalysisEngine(false, AnalysisEngine.Workload.WHOLE_GAME, 500); - - assertEquals(1, engine.startWholeGameRequest(List.of(requestedNode), 500, false)); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - - assertTrue(Lizzie.board.getHistory().previous().isPresent()); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(Lizzie.board.getHistory().next().isPresent()); - - assertTrue( - output.toString(StandardCharsets.UTF_8).contains("kata-get-rules\n"), - output.toString(StandardCharsets.UTF_8)); - String originalRules = - "{\"koRule\":\"POSITIONAL\",\"scoringRule\":\"AREA\",\"taxRule\":\"NONE\"}"; - assertTrue(dispatchExclusiveLine(foreground, "=830000000 " + originalRules)); - assertTrue( - output.toString(StandardCharsets.UTF_8).endsWith("830000001 boardsize 3\n"), - output.toString(StandardCharsets.UTF_8)); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void shutdownRequestCancelsPendingWholeGameTrackingHandoff() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = - new AnalysisEngine(false, AnalysisEngine.Workload.WHOLE_GAME, 500); - - assertEquals(1, engine.startWholeGameRequest(List.of(requestedNode), 500, false)); - Object target = getField(AnalysisEngine.class, engine, "pendingForegroundRequest"); - Leelaz.TrackingHandoffClaim claim = - (Leelaz.TrackingHandoffClaim) getField(target.getClass(), target, "claim"); - assertEquals(Leelaz.TrackingHandoffState.ACCEPTED_PENDING, claim.state()); - - engine.requestShutdown(); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertNull(getField(AnalysisEngine.class, engine, "pendingForegroundRequest")); - } - } - - @Test - void shutdownAfterForegroundActivationCheckReleasesNewHandoffSession() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - ActivationBarrierLeelaz foreground = - reusableForegroundEngine(new ActivationBarrierLeelaz(), true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false, AnalysisEngine.Workload.WHOLE_GAME, 500); - - assertEquals(1, engine.startWholeGameRequest(List.of(requestedNode), 500, false)); - AtomicReference workerFailure = new AtomicReference<>(); - Thread finalFence = - new Thread( - () -> { - try { - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - } catch (Throwable failure) { - workerFailure.set(failure); - } - }, - "tracking-handoff-final-fence"); - - finalFence.start(); - foreground.awaitForegroundActivation(); - engine.requestShutdown(); - foreground.continueForegroundActivation(); - finalFence.join(2000); - - assertFalse(finalFence.isAlive(), "final-fence worker did not finish"); - assertNull(workerFailure.get()); - assertTrue(foreground.cleanupUsedOriginalTarget()); - assertTrue( - output.toString(StandardCharsets.UTF_8).endsWith("800000004 stop\n"), - output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatchExclusiveLine(foreground, "=800000004")); - assertTrue(dispatchExclusiveLine(foreground, "")); - completeForegroundRestore(foreground); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void wholeGameTrackingHandoffReportsFinalFenceFailureOnce() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = - new AnalysisEngine(false, AnalysisEngine.Workload.WHOLE_GAME, 500); - AtomicInteger failures = new AtomicInteger(); - engine.setFailureCallback(failures::incrementAndGet); - - assertEquals(1, engine.startWholeGameRequest(List.of(requestedNode), 500, false)); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "?800000002 simulated final stop failure")); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - - assertEquals(1, failures.get()); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void mainlineCompletesAfterTrackingHandoffWhenEmptyBoardHasNoRequests() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - boardWithHistory(new BoardHistoryList(BoardData.empty(BOARD_SIZE, BOARD_SIZE))); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - TrackingWaitForAnalysis waitFrame = allocate(TrackingWaitForAnalysis.class); - waitFrame.setVisible(true); - engine.waitFrame = waitFrame; - AtomicInteger completions = new AtomicInteger(); - engine.setCompletionCallback(completions::incrementAndGet); - - engine.startRequest(-1, -1, true); - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue( - output.toString(StandardCharsets.UTF_8).endsWith("800000004 stop\n"), - output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatchExclusiveLine(foreground, "=800000004")); - assertTrue(dispatchExclusiveLine(foreground, "")); - completeForegroundRestore(foreground); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - - assertEquals(2, waitFrame.visibilityCalls); - assertFalse(waitFrame.lastVisible); - assertEquals(1, completions.get(), output.toString(StandardCharsets.UTF_8)); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void allBranchesCompletesAfterTrackingHandoffWhenEmptyBoardHasNoRequests() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - boardWithHistory(new BoardHistoryList(BoardData.empty(BOARD_SIZE, BOARD_SIZE))); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - AtomicInteger completions = new AtomicInteger(); - engine.setCompletionCallback(completions::incrementAndGet); - - engine.startRequestAllBranches(false); - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue( - output.toString(StandardCharsets.UTF_8).endsWith("800000004 stop\n"), - output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatchExclusiveLine(foreground, "=800000004")); - assertTrue(dispatchExclusiveLine(foreground, "")); - completeForegroundRestore(foreground); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - - assertEquals(1, completions.get(), output.toString(StandardCharsets.UTF_8)); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - @Test - void missingMainlineCompletesWhenAcquiringTrackingFillsLastRequestBeforeInitialFence() - throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - Leelaz.TrackingStreamLeaseAcquisition tracking = - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine engine = new AnalysisEngine(false); - AtomicInteger completions = new AtomicInteger(); - engine.setCompletionCallback(completions::incrementAndGet); - - assertEquals(1, engine.startRequestMissingMainline(false)); - requestedNode.getData().setPlayouts(120); - requestedNode.getData().analysisHeaderSlots = 3; - assertEquals("800000000 stop\n", output.toString(StandardCharsets.UTF_8)); - - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - completeForegroundRestore(foreground); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - - assertEquals(1, completions.get(), output.toString(StandardCharsets.UTF_8)); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-analyze")); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void missingMainlineCompletesWhenTrackingFillsLastRequestBeforeActivation() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - AtomicInteger completions = new AtomicInteger(); - engine.setCompletionCallback(completions::incrementAndGet); - - assertEquals(1, engine.startRequestMissingMainline(false)); - requestedNode.getData().setPlayouts(120); - requestedNode.getData().analysisHeaderSlots = 3; - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000004")); - assertTrue(dispatchExclusiveLine(foreground, "")); - completeForegroundRestore(foreground); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - - assertEquals(1, completions.get(), output.toString(StandardCharsets.UTF_8)); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void activeTrackingForegroundRequestFailsWithoutActivationAfterReaderRebind() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - initializeStreams(foreground, new ByteArrayOutputStream()); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void activeTrackingForegroundRequestFailsWithoutActivationAfterCurrentTerminal() - throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - GtpConsolePane previousConsole = Lizzie.gtpConsole; - try { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - setField( - Leelaz.class, foreground, "inputStream", new BufferedReader(new StringReader(""))); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - foreground.isNormalEnd = true; - invokeRead(foreground); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } finally { - Lizzie.gtpConsole = previousConsole; - } - } - } - - @Test - void activeTrackingForegroundRequestFailsWithoutActivationAfterFinalFenceFailure() - throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "?800000002 simulated final stop failure")); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isAnalysisInProgress()); - } - } - - @Test - void foregroundRetainedAndLifecycleContendersShareOneTrackingWinner() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - AnalysisEngine engine = new AnalysisEngine(false); - TrackingLizzieFrame frame = (TrackingLizzieFrame) Lizzie.frame; - - engine.startRequest(1, -1, false); - frame.startNewGame(); - Leelaz.ExclusiveGtpLifecycleReservation lifecycle = - foreground.beginExclusiveGtpLifecycleReservation(); - - assertNull(lifecycle); - assertEquals(1, frame.retainedModeConflictCount); - - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(foreground.hasExclusiveGtpLease()); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeClaimsActiveTrackingBeforeStartingAllBranchesRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - AnalysisEngine engine = new AnalysisEngine(false); - engine.startRequestAllBranches(false); - - assertEquals("800000000 stop\n", output.toString(StandardCharsets.UTF_8)); - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue(output.toString(StandardCharsets.UTF_8).contains("830000000 kata-get-rules\n")); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeAllowsOnlyTheFirstForegroundRequestToClaimTracking() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine first = new AnalysisEngine(false); - AnalysisEngine second = new AnalysisEngine(false); - - first.startRequest(1, -1, false); - second.startRequest(1, -1, false); - - assertEquals("800000000 stop\n", output.toString(StandardCharsets.UTF_8)); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE, - second.getForegroundLeaseAvailability()); - - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(output.toString(StandardCharsets.UTF_8).contains("830000000 kata-get-rules\n")); - closeExclusiveSessionForTest(foreground); - } - } - - @Test - void reuseModeDoesNotStartForegroundRequestAfterBoardContextChanges() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - singleUnanalyzedMoveNode(); - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - } - } - - @Test - void reuseModeDoesNotStartForegroundRequestAfterCurrentPositionChangesInPlace() - throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - Lizzie.board.getHistory().setStone(new int[] {0, 0}, Stone.BLACK); - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - } - } - - @Test - void reuseModeDoesNotStartForegroundRequestAfterNavigationReturnsToSameNode() - throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - BoardHistoryNode requestedNode = singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - assertTrue(Lizzie.board.previousMove(false)); - assertTrue(Lizzie.board.nextMove(false)); - assertSame(requestedNode, Lizzie.board.getHistory().getCurrentHistoryNode()); - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertFalse(output.toString(StandardCharsets.UTF_8).contains("kata-get-rules")); - assertFalse(foreground.hasExclusiveGtpWorkInProgress()); - } - } - - @Test - void failedNavigationAttemptDoesNotInvalidateForegroundRequest() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Lizzie.config.analysisReuseCurrentEngine = true; - singleUnanalyzedMoveNode(); - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - foreground.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - AnalysisEngine engine = new AnalysisEngine(false); - - engine.startRequest(1, -1, false); - assertFalse(Lizzie.board.nextMove(false)); - processCommandResponse(foreground, "=800000000"); - assertTrue(dispatchExclusiveLine(foreground, "")); - - assertTrue(output.toString(StandardCharsets.UTF_8).contains("830000000 kata-get-rules\n")); - closeExclusiveSessionForTest(foreground); - } - } @Test void reuseModeRejectsHumanSlAndContributionModes() throws Exception { @@ -1101,28 +464,6 @@ void benchmarkSuppressionRejectsLocalContributionBeforeReservedLifecycle() throw } } - @Test - void activeTrackingLetsContributionLifecycleRunImmediatelyOnce() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - Leelaz foreground = reusableForegroundEngine(true); - ByteArrayOutputStream output = installLeelazOutput(foreground); - Lizzie.leelaz = foreground; - activateTracking(foreground); - TrackingLizzieFrame frame = (TrackingLizzieFrame) Lizzie.frame; - - frame.startContributeEngine(); - - assertEquals(1, frame.contributionStarts); - assertEquals(0, frame.foregroundReservationConflictCount); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertNull(foreground.beginExclusiveGtpLifecycleReservation()); - assertTrue(dispatchExclusiveLine(foreground, "")); - assertTrue(dispatchExclusiveLine(foreground, "=800000002")); - assertTrue(dispatchExclusiveLine(foreground, "")); - } - } @Test void rejectedInteractiveGenmoveDoesNotLeaveInputCommandStateBehind() throws Exception { @@ -1847,12 +1188,28 @@ void localWholeGameUsesRootVisitsWithoutRequiringTopChildToReachTarget() throws assertEquals(1, requested); assertEquals(500, rootData.getPlayouts()); + assertEquals(500, rootData.rootVisits); assertEquals(300, rootData.bestMoves.get(0).playouts); assertTrue(rootData.hasCompletePrimaryAnalysis(500, false)); waitForMovelistRefreshThreads(); } } + @Test + void childResearchCannotSatisfyWholeGameRootBudget() throws Exception { + try (TestEnvironment env = TestEnvironment.open()) { + BoardData rootData = BoardData.empty(BOARD_SIZE, BOARD_SIZE); + BoardHistoryList history = new BoardHistoryList(rootData); + boardWithHistory(history); + TrackingAnalysisEngine engine = TrackingAnalysisEngine.create(); + engine.synchronousResponse = analysisResultWithRootVisits(1, 848, 100, 70.0); + engine.startWholeGameRequest(List.of(history.getStart()), 500, false); + assertEquals(0, rootData.getPlayouts()); + assertFalse(rootData.hasCompletePrimaryAnalysis(500, false)); + waitForMovelistRefreshThreads(); + } + } + @Test void emptyJsonPayloadCompletesTheBatchWithoutReportingFalseSuccess() throws Exception { try (TestEnvironment env = TestEnvironment.open()) { @@ -3783,37 +3140,8 @@ private static ByteArrayOutputStream installLeelazOutput(Leelaz engine) throws E return output; } - private static Leelaz.TrackingStreamLeaseAcquisition activateTracking(Leelaz engine) - throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(tracking.lease().send("kata-analyze B 10")); - return tracking; - } - private static void initializeStreams(Leelaz engine, ByteArrayOutputStream output) - throws Exception { - Method method = - Leelaz.class.getDeclaredMethod( - "initializeStreams", - java.io.InputStream.class, - java.io.OutputStream.class, - java.io.InputStream.class); - method.setAccessible(true); - method.invoke( - engine, - new ByteArrayInputStream(new byte[0]), - output, - new ByteArrayInputStream(new byte[0])); - } - private static void invokeRead(Leelaz engine) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("read"); - method.setAccessible(true); - method.invoke(engine); - } private static void closeExclusiveSessionForTest(Leelaz engine) throws Exception { if (engine == null) { @@ -4081,97 +3409,6 @@ private void failRestore() { } } - private static final class ActivationBarrierLeelaz extends Leelaz { - private final CountDownLatch foregroundActivationReached = new CountDownLatch(1); - private final CountDownLatch foregroundActivationMayContinue = new CountDownLatch(1); - private volatile TrackingHandoffTarget originalTarget; - private volatile TrackingHandoffTarget installedTarget; - private volatile boolean cleanupUsedOriginalTarget; - - private ActivationBarrierLeelaz() throws IOException { - super(""); - } - - @Override - public TrackingHandoffClaim claimTrackingHandoff(TrackingHandoffTarget target) { - TrackingHandoffTarget barrierTarget = - new TrackingHandoffTarget() { - @Override - public TrackingHandoffKind kind() { - return target.kind(); - } - - @Override - public boolean isCurrent() { - return target.isCurrent(); - } - - @Override - public void activate(TrackingHandoffActivation activation) { - target.activate( - new TrackingHandoffActivation() { - @Override - public boolean activateForegroundAnalysis( - java.util.function.Consumer lineConsumer, Runnable onClosed) { - foregroundActivationReached.countDown(); - awaitForegroundActivationRelease(); - return activation.activateForegroundAnalysis(lineConsumer, onClosed); - } - - @Override - public boolean completeRetainedEngineMode() { - return activation.completeRetainedEngineMode(); - } - - @Override - public EngineModeReservation beginRetainedEngineModeReservation() { - return activation.beginRetainedEngineModeReservation(); - } - }); - } - - @Override - public void fail(TrackingHandoffFailure failure) { - target.fail(failure); - } - }; - originalTarget = target; - installedTarget = barrierTarget; - return super.claimTrackingHandoff(barrierTarget); - } - - @Override - public boolean endForegroundAnalysisLease(Object owner, Runnable completion, Runnable failure) { - cleanupUsedOriginalTarget = owner == originalTarget; - return super.endForegroundAnalysisLease( - cleanupUsedOriginalTarget ? installedTarget : owner, completion, failure); - } - - private void awaitForegroundActivation() throws InterruptedException { - assertTrue( - foregroundActivationReached.await(2, TimeUnit.SECONDS), - "foreground activation did not reach the barrier"); - } - - private void continueForegroundActivation() { - foregroundActivationMayContinue.countDown(); - } - - private boolean cleanupUsedOriginalTarget() { - return cleanupUsedOriginalTarget; - } - - private void awaitForegroundActivationRelease() { - try { - if (!foregroundActivationMayContinue.await(2, TimeUnit.SECONDS)) { - throw new AssertionError("foreground activation barrier was not released"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new AssertionError("foreground activation barrier was interrupted", interrupted); - } - } - } private static final class TrackingWaitForAnalysis extends WaitForAnalysis { private int progressCalls; diff --git a/src/test/java/featurecat/lizzie/analysis/EngineManagerBenchmarkTest.java b/src/test/java/featurecat/lizzie/analysis/EngineManagerBenchmarkTest.java index 8b1105fc2..2f3694478 100644 --- a/src/test/java/featurecat/lizzie/analysis/EngineManagerBenchmarkTest.java +++ b/src/test/java/featurecat/lizzie/analysis/EngineManagerBenchmarkTest.java @@ -280,7 +280,11 @@ void switchingBackToGtpReapsToolAndRestoresBoard(boolean fastChange) throws Exce tool.awaitReady(); BenchmarkExecution old = tool.engine.benchmarkExecution(); assertTrue(manager.switchEngineIfAvailable(1, true)); - await(() -> manager.isEngineSwitchActive(1, true)); + try { await(() -> manager.isEngineSwitchActive(1, true)); } + catch (AssertionError failure) { + throw new AssertionError(Files.readString(gtp.path.resolve("stdin")) + + " failure=" + manager.engineSwitchUiSnapshot(true).failureDetail(), failure); + } assertFalse(tool.alive()); assertEquals(BenchmarkExecution.State.CANCELLED, old.snapshot().state()); assertTrue(Files.readString(gtp.path.resolve("stdin")).contains("clear_board")); diff --git a/src/test/java/featurecat/lizzie/analysis/EngineManagerInitialStartupSynchronizationTest.java b/src/test/java/featurecat/lizzie/analysis/EngineManagerInitialStartupSynchronizationTest.java index 841896e47..a58a72126 100644 --- a/src/test/java/featurecat/lizzie/analysis/EngineManagerInitialStartupSynchronizationTest.java +++ b/src/test/java/featurecat/lizzie/analysis/EngineManagerInitialStartupSynchronizationTest.java @@ -649,7 +649,8 @@ void foregroundActivationConvergesAfterDelayedReadyAndOrdinaryMoveNavigation() t } assertTrue( engine.analysisStarted.await(2, TimeUnit.SECONDS), - "analysis must start after the final board fence succeeds"); + "analysis must start after final fence; commands=" + engine.commands + + " phase=" + manager.engineSwitchUiSnapshot(true).phase()); assertTrue( manager.firstSynchronizationCompleted.await(2, TimeUnit.SECONDS), "production synchronization worker must complete before assertions"); @@ -4188,60 +4189,6 @@ void closeReleasesMirrorAdmissionAndCompletionClaimAfterTargetDetachError() } } - @Test - void lifecycleReservationsCloseRetainsFirstFailureAndSuppressesEveryLaterFailure() - throws Exception { - try (StartupTestEnvironment env = StartupTestEnvironment.open()) { - StartupSyncLeelaz current = new StartupSyncLeelaz(); - StartupSyncLeelaz target = new StartupSyncLeelaz(); - env.publish(current, boardWithHistory(emptyRootHistory(0))); - Leelaz.ExclusiveGtpLifecycleReservation currentReservation = - current.beginExclusiveGtpLifecycleReservation(); - Leelaz.ExclusiveGtpLifecycleReservation targetReservation = - target.beginExclusiveGtpLifecycleReservation(); - assertNotNull(currentReservation); - assertNotNull(targetReservation); - queueFailingCommandForLifecycleRelease(current, "name"); - queueFailingCommandForLifecycleRelease(target, "name"); - current.installCommandOutputForTest(new FailingOutputStream("current release failure")); - target.installCommandOutputForTest(new FailingOutputStream("target release failure")); - AssertionError gateFailure = new AssertionError("controlled interaction gate close failure"); - AtomicInteger gateCloseCount = new AtomicInteger(); - - Class reservationsType = - Class.forName("featurecat.lizzie.analysis.EngineManager$EngineLifecycleReservations"); - Constructor constructor = - reservationsType.getDeclaredConstructor( - Leelaz.ExclusiveGtpLifecycleReservation.class, - Leelaz.ExclusiveGtpLifecycleReservation.class); - constructor.setAccessible(true); - Object reservations = constructor.newInstance(currentReservation, targetReservation); - Field interactionGate = reservationsType.getDeclaredField("interactionGate"); - interactionGate.setAccessible(true); - interactionGate.set( - reservations, - (LizzieFrame.RestartInteractionGate) - () -> { - gateCloseCount.incrementAndGet(); - throw gateFailure; - }); - Method close = reservationsType.getDeclaredMethod("close"); - close.setAccessible(true); - - InvocationTargetException invocationFailure = - assertThrows(InvocationTargetException.class, () -> close.invoke(reservations)); - Throwable failure = invocationFailure.getCause(); - - assertTrue(failure instanceof RuntimeException); - assertTrue(failure.getMessage().contains("target release failure")); - assertEquals(2, failure.getSuppressed().length); - assertTrue(failure.getSuppressed()[0].getMessage().contains("current release failure")); - assertSame(gateFailure, failure.getSuppressed()[1]); - assertEquals(1, gateCloseCount.get()); - assertFalse(current.hasExclusiveGtpWorkInProgress()); - assertFalse(target.hasExclusiveGtpWorkInProgress()); - } - } @Test void captureFailureRetainsPrimaryFailureWhenReservationCleanupAlsoFails() throws Exception { diff --git a/src/test/java/featurecat/lizzie/analysis/EngineManagerLifecycleReservationTest.java b/src/test/java/featurecat/lizzie/analysis/EngineManagerLifecycleReservationTest.java index 953cca611..b8c7f1fa6 100644 --- a/src/test/java/featurecat/lizzie/analysis/EngineManagerLifecycleReservationTest.java +++ b/src/test/java/featurecat/lizzie/analysis/EngineManagerLifecycleReservationTest.java @@ -4788,122 +4788,6 @@ void configurationSwitchRejectsPreExistingMirrorReservationBeforeAnySwitchWork() } } - @Test - void killAllEnginesClaimsActiveTrackingAndRunsImmediatelyOnce() throws Exception { - assertDestructiveKillClaimsActiveTracking(true); - } - - @Test - void killThisEngineClaimsActiveTrackingAndRunsImmediatelyOnce() throws Exception { - assertDestructiveKillClaimsActiveTracking(false); - } - - @Test - void activeRestartResumesPonderAfterFinalBoardFenceAndRetiresTrackingOnRebind() - throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - boolean previousEmpty = EngineManager.isEmpty; - int previousEngineNo = EngineManager.currentEngineNo; - TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - CountingRestartGateFrame frame = allocate(CountingRestartGateFrame.class); - DeferredSwitchEngineManager manager = - new DeferredSwitchEngineManager(List.of(engine)); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(output)); - setCapabilityDiscoveryComplete(engine, true); - try { - Lizzie.leelaz = engine; - Lizzie.frame = frame; - EngineManager.isEmpty = false; - EngineManager.currentEngineNo = 0; - engine.Pondering(); - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - engine.emitPonderCommand = true; - engine.Pondering(); - - manager.reStartEngine(0); - - assertEquals(1, engine.shutdownCount); - assertEquals(1, manager.switchCount); - assertEquals(1, frame.beginCount); - assertFalse(tracking.lease().isOwned()); - assertTrue(engine.hasExclusiveGtpWorkInProgress()); - assertNotNull(manager.afterSync); - assertFalse(engine.isStarted()); - assertFalse(engine.isLoaded()); - engine.started = true; - engine.isLoaded = true; - engine.Pondering(); - manager.afterSync.run(); - assertNotNull(engine.confirmation); - assertTrue(engine.hasExclusiveGtpWorkInProgress()); - assertEquals(0, engine.ponderCount); - setLeelazField(engine, "currentCmdNum", 15); - setLeelazField(engine, "cmdNumber", 16); - engine.confirmation.run(); - assertFalse(engine.hasExclusiveGtpWorkInProgress()); - assertEquals(1, engine.ponderCount); - assertTrue(engine.ponderWhileLifecycleHeld); - assertEquals(17, getLeelazField(engine, "cmdNumber")); - assertTrue( - engine.isResponseUpToDate(), - "post-fence ponder must accept the first analysis info without another board action"); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - EngineManager.isEmpty = previousEmpty; - EngineManager.currentEngineNo = previousEngineNo; - } - } - - @Test - void activeRestartReleasesLifecycleAfterBoardFenceFailure() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - boolean previousEmpty = EngineManager.isEmpty; - int previousEngineNo = EngineManager.currentEngineNo; - TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - CountingRestartGateFrame frame = allocate(CountingRestartGateFrame.class); - DeferredSwitchEngineManager manager = - new DeferredSwitchEngineManager(List.of(engine)); - setLeelazField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); - setCapabilityDiscoveryComplete(engine, true); - try { - Lizzie.leelaz = engine; - Lizzie.frame = frame; - EngineManager.isEmpty = false; - EngineManager.currentEngineNo = 0; - activateTracking(engine); - - manager.reStartEngine(0); - - assertEquals(1, engine.shutdownCount); - assertEquals(1, frame.beginCount); - assertNotNull(manager.afterSync); - assertFalse(engine.isStarted()); - assertFalse(engine.isLoaded()); - engine.started = true; - engine.isLoaded = true; - engine.Pondering(); - manager.afterSync.run(); - assertNotNull(engine.rejection); - assertTrue(engine.hasExclusiveGtpWorkInProgress()); - - engine.rejection.accept("controlled board fence failure"); - - assertFalse(engine.hasExclusiveGtpWorkInProgress()); - assertFalse(engine.isLoaded()); - assertEquals(0, engine.ponderCount); - assertFalse(engine.hasUnrestoredReadBoardGmaState()); - assertEquals(1, manager.failureCount); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - EngineManager.isEmpty = previousEmpty; - EngineManager.currentEngineNo = previousEngineNo; - } - } @Test void unresponsiveRemoteAnalysisRestartsAndRestoresThroughExistingLifecycle() throws Exception { @@ -5139,59 +5023,6 @@ void switchKeepsCurrentAndTargetReservedUntilBoardSynchronizationCompletes() thr } } - @Test - void retainedSwitchRejectsOldTrackingOrdinaryCommandAtQueueGate() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Config previousConfig = Lizzie.config; - TrackingKillLeelaz current = new TrackingKillLeelaz(); - TrackingRestartActionLeelaz target = new TrackingRestartActionLeelaz(); - LifecycleFrame frame = allocate(LifecycleFrame.class); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - current.installCommandOutputForTest(new BufferedOutputStream(output)); - setCapabilityDiscoveryComplete(current, true); - DeferredSwitchEngineManager manager = - new DeferredSwitchEngineManager(List.of(current, target)); - try { - Lizzie.frame = frame; - Lizzie.leelaz = current; - Lizzie.config = allocate(Config.class); - activateTracking(current); - - manager.switchEngine(1, true); - target.started = true; - target.isLoaded = true; - target.Pondering(); - Lizzie.leelaz = target; - - assertEquals(1, manager.switchCount); - assertNotNull(manager.afterSync); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - current.sendCommand("stop"); - manager.afterSync.run(); - assertNotNull(target.confirmation); - target.confirmation.run(); - assertEquals(0, target.ponderCount, "regular switch must preserve its existing ponder path"); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - - assertTrue(dispatchExclusiveLine(current, "")); - assertTrue(dispatchExclusiveLine(current, "=800000002")); - assertTrue(dispatchExclusiveLine(current, "")); - assertFalse(current.hasExclusiveGtpWorkInProgress()); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8), - "an uncredentialed ordinary command rejected behind the lifecycle gate must not replay"); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - } - } @Test void configurationSwitchReportsReservationConflictWithoutGenericPopup() throws Exception { @@ -5751,7 +5582,8 @@ private void assertUnpreparedExplicitRestartFinalInitializationFailure( .filter( method -> method.getName().equals("releaseEngineLifecycleAfterBoardSync") - && method.getParameterCount() == 8) + && method.getParameterCount() == 7 + && method.getParameterTypes()[5] == Runnable.class) .findFirst() .orElseThrow(); callbackFactory.setAccessible(true); @@ -5765,7 +5597,6 @@ private void assertUnpreparedExplicitRestartFinalInitializationFailure( true, false, (Runnable) releaseCount::incrementAndGet, - false, null); if (callbackOnEdt) { @@ -5893,7 +5724,8 @@ private void assertBoardFenceSetupAndFailureCleanupErrorSettleOnce( .filter( method -> method.getName().equals("releaseEngineLifecycleAfterBoardSync") - && method.getParameterCount() == 8) + && method.getParameterCount() == 7 + && method.getParameterTypes()[5] == Runnable.class) .findFirst() .orElseThrow(); callbackFactory.setAccessible(true); @@ -5911,7 +5743,6 @@ private void assertBoardFenceSetupAndFailureCleanupErrorSettleOnce( releaseCount.incrementAndGet(); throw cleanupFailure; }, - false, lifecycleRestore); AssertionError thrown = assertThrows(AssertionError.class, completion::run); @@ -6164,11 +5995,11 @@ private void assertSecondaryExplicitRestartBoardFenceFailClosed(boolean failOnSe EngineManager.currentEngineNo = 0; EngineManager.currentEngineNo2 = 1; Lizzie.engineStartupStatus.ready(); + secondary.restartOutput = gatedOutput; manager.reStartEngine2(); assertEquals(1, secondary.shutdownCount); assertNotNull(manager.afterSync); - setLeelazField(secondary, "outputStream", new BufferedOutputStream(gatedOutput)); fenceThread = new Thread(() -> manager.afterSync.run(), "secondary-restart-board-fence"); fenceThread.start(); @@ -6203,7 +6034,6 @@ private void assertSecondaryExplicitRestartBoardFenceFailClosed(boolean failOnSe assertEquals(0, primary.ponderCount); assertEquals(0, secondary.ponderCount); assertFalse(secondary.responseWatermarkWhileLifecycleHeld); - assertFalse(secondary.isResponseUpToDate()); assertEquals(1, manager.failureCount); assertFalse(primary.hasExclusiveGtpWorkInProgress()); assertFalse(secondary.hasExclusiveGtpWorkInProgress()); @@ -6255,7 +6085,7 @@ private void assertExplicitRestartWaitsForBoardFence(boolean resumePonder) throw BottomToolbar previousToolbar = LizzieFrame.toolbar; boolean previousEmpty = EngineManager.isEmpty; TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - CountingRestartGateFrame frame = allocate(CountingRestartGateFrame.class); + CountingRestartFrame frame = allocate(CountingRestartFrame.class); CountingRestartMenu menu = allocate(CountingRestartMenu.class); BottomToolbar toolbar = allocate(SilentSwitchToolbar.class); PreparedRestoreBoard board = preparedRestoreBoard(); @@ -6336,7 +6166,6 @@ void restartSynchronizationPropagatesReceiptIntoTheFinalBoardFence() throws Exce Lizzie.leelaz = engine; Leelaz.ExclusiveGtpLifecycleReservation reservation = null; try { - activateTracking(engine); reservation = engine.beginExclusiveGtpLifecycleReservation(); assertNotNull(reservation); rebindReader(engine); @@ -6365,7 +6194,6 @@ void restartSynchronizationFailureRetiresReceiptWithoutCreatingGmaQuarantine() t Lizzie.leelaz = engine; Leelaz.ExclusiveGtpLifecycleReservation reservation = null; try { - activateTracking(engine); reservation = engine.beginExclusiveGtpLifecycleReservation(); assertNotNull(reservation); rebindReader(engine); @@ -6401,7 +6229,6 @@ void staleRestartFailureReceiptCannotUnloadReboundRuntime() throws Exception { Lizzie.leelaz = engine; Leelaz.ExclusiveGtpLifecycleReservation reservation = null; try { - activateTracking(engine); reservation = engine.beginExclusiveGtpLifecycleReservation(); assertNotNull(reservation); rebindReader(engine); @@ -6431,7 +6258,6 @@ void restartReceiptIsDetachedFromTheReaderBindingWhenLifecycleEnds() throws Exce setLeelazField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); Lizzie.leelaz = engine; try { - activateTracking(engine); Leelaz.ExclusiveGtpLifecycleReservation reservation = engine.beginExclusiveGtpLifecycleReservation(); assertNotNull(reservation); @@ -6447,129 +6273,6 @@ void restartReceiptIsDetachedFromTheReaderBindingWhenLifecycleEnds() throws Exce } } - @Test - void restartGateFailureReleasesLifecycleBeforeDestructiveWork() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - boolean previousEmpty = EngineManager.isEmpty; - TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); - setCapabilityDiscoveryComplete(engine, true); - GateFailureEngineManager manager = new GateFailureEngineManager(List.of(engine)); - try { - Lizzie.leelaz = engine; - Lizzie.frame = null; - EngineManager.isEmpty = false; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - Lizzie.frame = allocate(FailingRestartGateFrame.class); - - manager.reStartEngine(0); - - assertEquals(0, engine.shutdownCount); - assertEquals(1, manager.failureCount); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse((boolean) getLeelazField(engine, "exclusiveGtpLifecycleTransition")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(dispatchExclusiveLine(engine, "=800000002")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertFalse(tracking.lease().isOwned()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - EngineManager.isEmpty = previousEmpty; - } - } - - @Test - void ordinaryRestartGateErrorReleasesLifecycleAndPropagatesOriginalError() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Board previousBoard = Lizzie.board; - boolean previousEmpty = EngineManager.isEmpty; - int previousEngineNo = EngineManager.currentEngineNo; - TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); - setCapabilityDiscoveryComplete(engine, true); - AssertionError gateFailure = new AssertionError("controlled ordinary restart gate error"); - GateFailureEngineManager manager = new GateFailureEngineManager(List.of(engine)); - try { - Lizzie.leelaz = engine; - Lizzie.frame = null; - Lizzie.board = null; - EngineManager.isEmpty = false; - EngineManager.currentEngineNo = 0; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - Lizzie.frame = allocate(ErrorRestartGateFrame.class); - ((ErrorRestartGateFrame) Lizzie.frame).failure = gateFailure; - - AssertionError observed = - assertThrows(AssertionError.class, () -> manager.reStartEngine(0)); - - assertSame(gateFailure, observed); - assertEquals(0, engine.shutdownCount); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse((boolean) getLeelazField(engine, "exclusiveGtpLifecycleTransition")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(dispatchExclusiveLine(engine, "=800000002")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertFalse(tracking.lease().isOwned()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.board = previousBoard; - EngineManager.isEmpty = previousEmpty; - EngineManager.currentEngineNo = previousEngineNo; - } - } - - @Test - void preparedRestartGateErrorClosesStartupSynchronizationAndPropagatesOriginalError() - throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Board previousBoard = Lizzie.board; - Config previousConfig = Lizzie.config; - boolean previousEmpty = EngineManager.isEmpty; - int previousEngineNo = EngineManager.currentEngineNo; - TrackingRestartActionLeelaz engine = new TrackingRestartActionLeelaz(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); - setCapabilityDiscoveryComplete(engine, true); - AssertionError gateFailure = new AssertionError("controlled prepared restart gate error"); - GateFailureEngineManager manager = new GateFailureEngineManager(List.of(engine)); - try { - Config config = allocate(Config.class); - config.extraMode = ExtraMode.Normal; - Lizzie.config = config; - Lizzie.leelaz = engine; - Lizzie.frame = null; - Lizzie.board = preparedRestoreBoard(); - EngineManager.isEmpty = false; - EngineManager.currentEngineNo = 0; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - Lizzie.frame = allocate(ErrorRestartGateFrame.class); - ((ErrorRestartGateFrame) Lizzie.frame).failure = gateFailure; - - AssertionError observed = - assertThrows(AssertionError.class, () -> manager.reStartEngine(0)); - - assertSame(gateFailure, observed); - assertEquals(0, engine.shutdownCount); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertFalse((boolean) getLeelazField(engine, "exclusiveGtpLifecycleTransition")); - assertNull(getLeelazField(engine, "initialEngineSyncAdmission")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(dispatchExclusiveLine(engine, "=800000002")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertFalse(tracking.lease().isOwned()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.board = previousBoard; - Lizzie.config = previousConfig; - EngineManager.isEmpty = previousEmpty; - EngineManager.currentEngineNo = previousEngineNo; - } - } @Test void secondaryRestartConflictDoesNotShutDownSecondaryEngine() throws Exception { @@ -8015,49 +7718,6 @@ private static Leelaz unavailableStartedEngine() throws Exception { return engine; } - private static void assertDestructiveKillClaimsActiveTracking(boolean killAll) throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - JFontMenu previousEngineMenu = Menu.engineMenu; - boolean previousEmpty = EngineManager.isEmpty; - int previousEngineNo = EngineManager.currentEngineNo; - TrackingKillLeelaz engine = new TrackingKillLeelaz(); - LifecycleFrame frame = allocate(LifecycleFrame.class); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(output)); - setCapabilityDiscoveryComplete(engine, true); - try { - Lizzie.frame = frame; - Lizzie.leelaz = engine; - Menu.engineMenu = new JFontMenu(); - EngineManager.isEmpty = false; - EngineManager.currentEngineNo = 0; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - EngineManager manager = new EngineManager(List.of(engine)); - - if (killAll) { - assertTrue(manager.killAllEngines()); - } else { - manager.killThisEngines(); - } - - assertEquals(1, engine.forceQuitCount); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertEquals(1, frame.trackingInvalidationCount); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(dispatchExclusiveLine(engine, "=800000002")); - assertTrue(dispatchExclusiveLine(engine, "")); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Menu.engineMenu = previousEngineMenu; - EngineManager.isEmpty = previousEmpty; - EngineManager.currentEngineNo = previousEngineNo; - } - } @SuppressWarnings("unchecked") private static T allocate(Class type) throws Exception { @@ -8432,11 +8092,6 @@ private static boolean hasRestartBootstrapReceiptContext(Leelaz engine) { } } - private static boolean dispatchExclusiveLine(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } private static void processCommandResponse(Leelaz engine, String line) throws Exception { Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); @@ -8473,15 +8128,6 @@ private static void setCapabilityDiscoveryComplete(Leelaz engine, boolean value) field.setBoolean(engine, value); } - private static Leelaz.TrackingStreamLeaseAcquisition activateTracking(Leelaz engine) - throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(tracking.lease().send("kata-analyze B 10")); - return tracking; - } private static void rebindReader(Leelaz engine) { rebindReader(engine, new ByteArrayOutputStream()); @@ -8996,7 +8642,7 @@ protected void switchEngineInternal( Lizzie.leelaz2 = target; target.started = true; target.isLoaded = true; - this.afterSync = afterSync; + this.afterSync = target.withCurrentRestartBootstrapReceipt(afterSync); } @Override @@ -9061,22 +8707,6 @@ public synchronized ExclusiveGtpLifecycleReservation beginExclusiveGtpLifecycleR } - private static final class TrackingKillLeelaz extends Leelaz { - private int forceQuitCount; - - private TrackingKillLeelaz() throws Exception { - super(""); - started = true; - isLoaded = true; - isKatago = true; - commandLists.addAll(List.of("stop", "boardsize", "komi", "kata-analyze")); - } - - @Override - public void forceQuit() { - forceQuitCount++; - } - } private static final class TrackingRestartActionLeelaz extends Leelaz { private int shutdownCount; @@ -9096,6 +8726,7 @@ private static final class TrackingRestartActionLeelaz extends Leelaz { private Runnable onResponseWatermark; private boolean useRealBoardSynchronizationFence; private long boardSynchronizationTimeoutMillis = -1L; + private java.io.OutputStream restartOutput; private TrackingRestartActionLeelaz() throws Exception { super(""); @@ -9108,7 +8739,8 @@ private TrackingRestartActionLeelaz() throws Exception { @Override public void shutdown() { shutdownCount++; - rebindReader(this); + if (restartOutput == null) rebindReader(this); + else installFreshCommandOutputForTest(restartOutput); } @Override @@ -9174,17 +8806,6 @@ void initializeAfterExplicitRestartBoardSynchronization(boolean resumePonder) { } } - private static final class LifecycleFrame extends LizzieFrame { - private int trackingInvalidationCount; - - @Override - public void invalidateTrackingAnalysis() { - trackingInvalidationCount++; - } - - @Override - public void refresh() {} - } private static final class SilentSwitchFrame extends LizzieFrame { private int reSetLocCount; @@ -11429,7 +11050,7 @@ private RestartIndexTestEnvironment( config.fastChange = true; config.extraMode = ExtraMode.Double_Engine; Lizzie.config = config; - Lizzie.frame = allocate(CountingRestartGateFrame.class); + Lizzie.frame = allocate(CountingRestartFrame.class); LizzieFrame.toolbar = allocate(SilentSwitchToolbar.class); LizzieFrame.menu = allocate(SilentUpdateMenu.class); Menu.engineMenu = new SilentJFontMenu(); @@ -11602,20 +11223,9 @@ protected void showSetupModeEngineUnavailable() { } } - private static final class CountingRestartGateFrame extends LizzieFrame { - private int beginCount; + private static final class CountingRestartFrame extends LizzieFrame { private int reSetLocCount; - @Override - public boolean isDisplayable() { - return true; - } - - @Override - public RestartInteractionGate beginRestartInteractionGate() { - beginCount++; - return () -> {}; - } @Override public void reSetLoc() { @@ -11652,44 +11262,6 @@ public void updateMenuStatusForEngine() { } } - private static final class FailingRestartGateFrame extends LizzieFrame { - @Override - public boolean isDisplayable() { - return true; - } - - @Override - public RestartInteractionGate beginRestartInteractionGate() { - throw new IllegalStateException("controlled restart gate failure"); - } - } - - private static final class ErrorRestartGateFrame extends LizzieFrame { - private AssertionError failure; - - @Override - public boolean isDisplayable() { - return true; - } - - @Override - public RestartInteractionGate beginRestartInteractionGate() { - throw failure; - } - } - - private static final class GateFailureEngineManager extends EngineManager { - private int failureCount; - - private GateFailureEngineManager(List engines) { - super(engines); - } - - @Override - protected void showEngineSynchronizationFailure(Leelaz engine) { - failureCount++; - } - } private static final class OrderedLifecycleLeelaz extends Leelaz { private final String name; diff --git a/src/test/java/featurecat/lizzie/analysis/ExactSnapshotEngineRestoreContractTest.java b/src/test/java/featurecat/lizzie/analysis/ExactSnapshotEngineRestoreContractTest.java index daae8d70a..267c8bac9 100644 --- a/src/test/java/featurecat/lizzie/analysis/ExactSnapshotEngineRestoreContractTest.java +++ b/src/test/java/featurecat/lizzie/analysis/ExactSnapshotEngineRestoreContractTest.java @@ -2504,7 +2504,7 @@ private ProductionResyncPonderLeelaz() throws IOException { } @Override - public boolean isPonderingOrWasPonderingBeforeTracking() { + public boolean isPondering() { return true; } diff --git a/src/test/java/featurecat/lizzie/analysis/ExactSnapshotRestoreProtocolFixture.java b/src/test/java/featurecat/lizzie/analysis/ExactSnapshotRestoreProtocolFixture.java index 0f8ea37fa..a6638f752 100644 --- a/src/test/java/featurecat/lizzie/analysis/ExactSnapshotRestoreProtocolFixture.java +++ b/src/test/java/featurecat/lizzie/analysis/ExactSnapshotRestoreProtocolFixture.java @@ -4,13 +4,23 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; /** Test-only transport that exercises Leelaz's real command queue and response routing. */ public final class ExactSnapshotRestoreProtocolFixture { private ExactSnapshotRestoreProtocolFixture() {} public static Transport install(Leelaz engine, CommandBehavior behavior) { - Transport transport = new Transport(engine, behavior); + Transport transport = new Transport(engine, behavior, false); + engine.installCommandOutputForTest(transport); + return transport; + } + + public static Transport installAsync(Leelaz engine, CommandBehavior behavior) { + Transport transport = new Transport(engine, behavior, true); engine.installCommandOutputForTest(transport); return transport; } @@ -44,10 +54,21 @@ public static final class Transport extends OutputStream { private final StringBuilder current = new StringBuilder(); private final List commands = new ArrayList<>(); private final List rawCommands = new ArrayList<>(); + private final ExecutorService responseReader; + private final AtomicReference responseFailure = new AtomicReference<>(); - private Transport(Leelaz engine, CommandBehavior behavior) { + private Transport(Leelaz engine, CommandBehavior behavior, boolean asynchronous) { this.engine = engine; this.behavior = behavior; + responseReader = + asynchronous + ? Executors.newSingleThreadExecutor( + task -> { + Thread thread = new Thread(task, "snapshot-fixture-reader"); + thread.setDaemon(true); + return thread; + }) + : null; } @Override @@ -67,7 +88,7 @@ public synchronized void flush() throws IOException { try { Response response = behavior.onCommand(commandPayload(commandLine)); if (response != null) { - invokeResponse(responseLine(commandLine, response)); + respond(responseLine(commandLine, response)); } } catch (IOException failure) { throw failure; @@ -84,8 +105,48 @@ public synchronized List rawCommands() { return List.copyOf(rawCommands); } - private void invokeResponse(String responseLine) { - engine.processCommandResponseLineForTest(responseLine); + public void respond(String responseLine) { + if (responseReader == null) { + engine.processCommandResponseLineForTest(responseLine); + } else { + // Real engine responses arrive on a reader, never inside the physical write lock. + responseReader.execute( + () -> { + try { + engine.processCommandResponseLineForTest(responseLine); + } catch (Throwable failure) { + responseFailure.compareAndSet(null, failure); + } + }); + } + } + + @Override + public void close() throws IOException { + if (responseReader == null) return; + responseReader.shutdown(); + try { + if (!responseReader.awaitTermination(3, TimeUnit.SECONDS)) { + responseReader.shutdownNow(); + throw new IOException("Snapshot fixture reader did not terminate"); + } + } catch (InterruptedException interrupted) { + responseReader.shutdownNow(); + Thread.currentThread().interrupt(); + throw new IOException(interrupted); + } + if (responseFailure.get() != null) { + throw new IOException("Snapshot fixture reader failed", responseFailure.get()); + } + } + + public void awaitResponses() throws Exception { + if (responseReader != null) { + responseReader.submit(() -> {}).get(3, TimeUnit.SECONDS); + } + if (responseFailure.get() != null) { + throw new AssertionError("Snapshot fixture reader failed", responseFailure.get()); + } } private static String commandPayload(String commandLine) { diff --git a/src/test/java/featurecat/lizzie/analysis/LeelazOrdinaryResponseSettlementTest.java b/src/test/java/featurecat/lizzie/analysis/LeelazOrdinaryResponseSettlementTest.java index 9a372585c..ccc6b7d27 100644 --- a/src/test/java/featurecat/lizzie/analysis/LeelazOrdinaryResponseSettlementTest.java +++ b/src/test/java/featurecat/lizzie/analysis/LeelazOrdinaryResponseSettlementTest.java @@ -25,6 +25,7 @@ void settledOrdinaryAnalyzeAcceptsItsFirstInfoLine() throws Exception { Config previousConfig = Lizzie.config; LizzieFrame previousFrame = Lizzie.frame; BottomToolbar previousToolbar = LizzieFrame.toolbar; + featurecat.lizzie.gui.Menu previousMenu = LizzieFrame.menu; Leelaz engine = new Leelaz(""); ByteArrayOutputStream output = new ByteArrayOutputStream(); try { @@ -38,9 +39,13 @@ void settledOrdinaryAnalyzeAcceptsItsFirstInfoLine() throws Exception { setField(engine, "cmdNumber", 16); Lizzie.leelaz = engine; Lizzie.config = allocate(Config.class); + Lizzie.config.enableLizzieCache = true; + Lizzie.config.limitPlayout = true; + Lizzie.config.limitPlayouts = 500; Lizzie.board = new Board(); AcceptingInfoFrame frame = allocate(AcceptingInfoFrame.class); Lizzie.frame = frame; + LizzieFrame.menu = allocate(SilentMenu.class); LizzieFrame.toolbar = allocate(BottomToolbar.class); engine.sendCommand("kata-analyze W 10"); @@ -48,19 +53,50 @@ void settledOrdinaryAnalyzeAcceptsItsFirstInfoLine() throws Exception { assertEquals("kata-analyze W 10\n", output.toString(StandardCharsets.UTF_8)); assertFalse(engine.isResponseUpToDate()); engine.setResponseUpToDate(); - parseInfo( - engine, "info move D4 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv D4"); + String upstream; + try (var input = getClass().getResourceAsStream("/analysis/katago-root-focus-info.txt")) { + upstream = new String(input.readAllBytes(), StandardCharsets.UTF_8).trim(); + } + parseInfo(engine, upstream); + assertEquals(100, Lizzie.board.getData().getPlayouts()); + assertEquals(100, Lizzie.board.getData().rootVisits); + MoveData c3 = engine.getBestMoves().stream() + .filter(move -> move.coordinate.equals("C3")).findFirst().orElseThrow(); + assertEquals(368, c3.playouts); + assertEquals(0, c3.edgeVisits); + assertEquals(10, c3.order); + assertFalse(engine.isStopPonderingByLimit()); - assertEquals(1, engine.getBestMoves().size()); - assertEquals("D4", engine.getBestMoves().get(0).coordinate); - assertEquals(40, engine.getBestMoves().get(0).playouts); - assertEquals(1, frame.analysisRefreshCount); + parseInfo(engine, "info move D4 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv D4" + + " rootInfo visits 100 winrate 0.9 scoreLead 20"); + assertEquals(51.0, Lizzie.board.getData().winrate, 0.001); + assertEquals(2.5, Lizzie.board.getData().scoreMean, 0.001); + parseInfo(engine, "info move D4 visits 40 winrate 0.66 scoreLead 3.5 prior 0.2 pv D4 C3" + + " rootInfo visits 100 winrate 0.9 scoreLead 20"); + assertEquals(66.0, Lizzie.board.getData().winrate, 0.001); + assertEquals(List.of("D4", "C3"), Lizzie.board.getData().bestMoves.get(0).variation); + + engine.sendCommand("kata-analyze W 11"); + engine.setResponseUpToDate(); + parseInfo(engine, "info move D4 visits 50000 winrate 0.8 pv D4 rootInfo visits 50"); + assertEquals(100, Lizzie.board.getData().getPlayouts()); + assertEquals(66.0, Lizzie.board.getData().winrate, 0.001); + parseInfo(engine, "info move D4 visits 50000 winrate 0.8 pv D4 rootInfo visits 100"); + assertEquals(66.0, Lizzie.board.getData().winrate, 0.001); + Lizzie.board.getData().isChanged = true; + parseInfo(engine, "info move D4 visits 50000 winrate 0.8 pv D4 rootInfo visits 50"); + assertEquals(50, Lizzie.board.getData().rootVisits); + assertEquals(80.0, Lizzie.board.getData().winrate, 0.001); + parseInfo(engine, "info move D4 visits 50000 winrate 0.8 pv D4 rootInfo visits 501"); + org.junit.jupiter.api.Assertions.assertTrue(engine.isStopPonderingByLimit()); + org.junit.jupiter.api.Assertions.assertTrue(output.toString(StandardCharsets.UTF_8).endsWith("stop\n")); } finally { Lizzie.leelaz = previousEngine; Lizzie.board = previousBoard; Lizzie.config = previousConfig; Lizzie.frame = previousFrame; LizzieFrame.toolbar = previousToolbar; + LizzieFrame.menu = previousMenu; } } @@ -83,6 +119,11 @@ private static T allocate(Class type) throws Exception { return (T) ((sun.misc.Unsafe) field.get(null)).allocateInstance(type); } + private static final class SilentMenu extends featurecat.lizzie.gui.Menu { + @Override + public void toggleEngineMenuStatus(boolean pondering, boolean thinking) {} + } + private static final class AcceptingInfoFrame extends LizzieFrame { private int analysisRefreshCount; diff --git a/src/test/java/featurecat/lizzie/analysis/LeelazReadBoardGmaTest.java b/src/test/java/featurecat/lizzie/analysis/LeelazReadBoardGmaTest.java index 8126f3691..989056de8 100644 --- a/src/test/java/featurecat/lizzie/analysis/LeelazReadBoardGmaTest.java +++ b/src/test/java/featurecat/lizzie/analysis/LeelazReadBoardGmaTest.java @@ -49,67 +49,6 @@ import org.junit.jupiter.params.provider.ValueSource; class LeelazReadBoardGmaTest { - @Test - void readBoardGmaUsesRetainedHandoffWhenTrackingOwnsTheStream() throws Exception { - try (Harness harness = Harness.open()) { - Leelaz engine = readyReadBoardGmaEngine(); - Lizzie.leelaz = engine; - RecordingOutputStream output = new RecordingOutputStream(); - setOutputStream(engine, output); - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease( - line -> {}, - lease -> lease.send("kata-analyze 10 allow B D4 1 allow W D4 1"), - lease -> {}, - disposition -> {}); - completeTrackingInitialFence(engine, 800000000); - AtomicInteger activations = new AtomicInteger(); - AtomicInteger failures = new AtomicInteger(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - engine.activateReadBoardGmaAfterTracking(this, "B", 5, 1000, true, activation); - activations.incrementAndGet(); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failures.incrementAndGet(); - } - }; - - Leelaz.TrackingHandoffClaim claim = engine.claimTrackingHandoff(target); - - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertEquals( - Leelaz.TrackingHandoffAvailability.BUSY, - engine.claimTrackingHandoff(target).availability()); - assertFalse( - output.commands().stream().anyMatch(command -> command.contains("kata-get-param"))); - - completeTrackingFinalFence(engine, 800000002); - - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertEquals(1, activations.get()); - assertEquals(0, failures.get()); - assertTrue(output.commands().contains("kata-get-param ponderingEnabled")); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.READBOARD_GMA, - engine.previewForegroundAnalysisLeaseAvailability()); - assertTrue(tracking.lease().failureReason().isEmpty()); - } - } @Test void retiringActiveReadBoardGmaReleasesReservationAndQuarantinesDirtyRuntimeState() @@ -3893,24 +3832,7 @@ private static Leelaz readyReadBoardGmaEngine() throws Exception { return engine; } - private static void completeTrackingInitialFence(Leelaz engine, int commandId) throws Exception { - assertFalse(dispatchExclusiveGtpLine(engine, "=" + commandId)); - invokeProcessCommandResponseLine(engine, "=" + commandId); - assertTrue(dispatchExclusiveGtpLine(engine, "")); - } - - private static void completeTrackingFinalFence(Leelaz engine, int commandId) throws Exception { - assertTrue(dispatchExclusiveGtpLine(engine, "")); - assertTrue(dispatchExclusiveGtpLine(engine, "=" + commandId)); - assertTrue(dispatchExclusiveGtpLine(engine, "")); - } - private static boolean dispatchExclusiveGtpLine(Leelaz engine, String line) throws Exception { - java.lang.reflect.Method method = - Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } private static void configureReadyReadBoardGmaEngine(Leelaz engine) throws Exception { engine.started = true; diff --git a/src/test/java/featurecat/lizzie/analysis/LeelazTrackingStreamLeaseTest.java b/src/test/java/featurecat/lizzie/analysis/LeelazTrackingStreamLeaseTest.java deleted file mode 100644 index f096da8a5..000000000 --- a/src/test/java/featurecat/lizzie/analysis/LeelazTrackingStreamLeaseTest.java +++ /dev/null @@ -1,3481 +0,0 @@ -package featurecat.lizzie.analysis; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import featurecat.lizzie.Config; -import featurecat.lizzie.ExtraMode; -import featurecat.lizzie.Lizzie; -import featurecat.lizzie.enginegame.EngineGamePlans; -import featurecat.lizzie.gui.GtpConsolePane; -import featurecat.lizzie.gui.LizzieFrame; -import featurecat.lizzie.gui.Menu; -import featurecat.lizzie.gui.WinrateGraph; -import featurecat.lizzie.rules.Board; -import featurecat.lizzie.rules.Stone; -import featurecat.lizzie.rules.Zobrist; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.StringReader; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import sun.misc.Unsafe; - -class LeelazTrackingStreamLeaseTest { - private final List createdEngines = new ArrayList<>(); - - @AfterEach - void closeCreatedExclusiveSessions() throws Exception { - for (Leelaz engine : createdEngines) { - closeExclusiveSessionForTest(engine); - } - createdEngines.clear(); - } - - @Test - void unsafeAllocatedEngineLazilyInitializesOneAnalysisInfoMutationLock() throws Exception { - Leelaz engine = allocate(Leelaz.class); - Field lockField = Leelaz.class.getDeclaredField("analysisInfoMutationLock"); - lockField.setAccessible(true); - assertEquals(null, lockField.get(engine)); - - engine.clearBestMoves(); - - Object initializedLock = lockField.get(engine); - assertTrue(initializedLock != null); - assertEquals(List.of(), engine.getBestMoves()); - engine.clearBestMoves(); - assertSame(initializedLock, lockField.get(engine)); - } - - @Test - void testOutputReplacementKeepsReaderIncarnationAndBindingCoherent() throws Exception { - Leelaz engine = new Leelaz(""); - Object incarnation = engine.currentEngineIncarnation(); - ByteArrayOutputStream replacement = new ByteArrayOutputStream(); - - engine.installCommandOutputForTest(replacement); - - assertSame(incarnation, engine.currentEngineIncarnation()); - Field outerOutputField = Leelaz.class.getDeclaredField("outputStream"); - outerOutputField.setAccessible(true); - Field bindingOutputField = incarnation.getClass().getDeclaredField("output"); - bindingOutputField.setAccessible(true); - Field rawOutputField = incarnation.getClass().getDeclaredField("rawOutput"); - rawOutputField.setAccessible(true); - assertSame(outerOutputField.get(engine), bindingOutputField.get(incarnation)); - assertSame(replacement, rawOutputField.get(incarnation)); - } - - @Test - void trackingLeaseOwnsOnlyItsStreamAndReleasesWithoutBoardRestoreOrPonder() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - Board previousBoard = Lizzie.board; - LizzieFrame previousFrame = Lizzie.frame; - Config previousConfig = Lizzie.config; - GtpConsolePane previousGtpConsole = Lizzie.gtpConsole; - Leelaz engine = reusableLocalKatago(); - ByteArrayOutputStream output = installOutput(engine); - List lines = new ArrayList<>(); - AtomicReference readyLease = new AtomicReference<>(); - AtomicInteger closed = new AtomicInteger(); - RecordingBoard board = allocate(RecordingBoard.class); - try { - Lizzie.leelaz = engine; - Lizzie.board = board; - Lizzie.frame = allocate(LizzieFrame.class); - Lizzie.config = allocate(Config.class); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - engine.Pondering(); - - Leelaz.TrackingStreamLeaseAcquisition acquisition = - engine.acquireTrackingStreamLease( - lines::add, readyLease::set, lease -> closed.incrementAndGet()); - - assertEquals(Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, acquisition.availability()); - assertSame(engine, acquisition.receipt().engine()); - assertTrue(acquisition.receipt().engineIncarnation() > 0L); - assertTrue(acquisition.receipt().wasPondering()); - assertEquals("800000000 stop\n", output.toString(StandardCharsets.UTF_8)); - assertFalse(dispatch(engine, "info move Q16 visits 2")); - assertFalse(dispatch(engine, "= old-response")); - assertFalse(dispatch(engine, "")); - - assertFalse(dispatch(engine, "=800000000")); - processCommandResponse(engine, "=800000000"); - assertEquals(null, readyLease.get()); - assertTrue(dispatch(engine, "")); - assertSame(acquisition.lease(), readyLease.get()); - - assertTrue(acquisition.lease().send("kata-analyze 10 allow B D4 1 allow W D4 1")); - assertEquals( - "800000000 stop\n800000001 kata-analyze 10 allow B D4 1 allow W D4 1\n", - output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatch(engine, "=800000001")); - assertTrue(dispatch(engine, "info move D4 visits 10")); - assertEquals(List.of("=800000001", "info move D4 visits 10"), lines); - - engine.sendCommand("version"); - assertFalse(acquisition.lease().release()); - assertEquals( - "800000000 stop\n" - + "800000001 kata-analyze 10 allow B D4 1 allow W D4 1\n" - + "800000002 stop\n", - output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatch(engine, "")); - assertTrue(dispatch(engine, "=800000002")); - assertEquals(0, closed.get()); - assertTrue(dispatch(engine, "")); - - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals( - "800000000 stop\n" - + "800000001 kata-analyze 10 allow B D4 1 allow W D4 1\n" - + "800000002 stop\n" - + "version\n", - output.toString(StandardCharsets.UTF_8)); - assertFalse(engine.isPondering()); - assertEquals(0, board.resendCount); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.board = previousBoard; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - } - } - - @Test - void trackingLeaseReportsInitialAndFinalFenceFailuresWithoutReopeningTheStream() - throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Config previousConfig = Lizzie.config; - GtpConsolePane previousGtpConsole = Lizzie.gtpConsole; - try { - Lizzie.frame = allocate(LizzieFrame.class); - Lizzie.config = allocate(Config.class); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - - Leelaz initialFailureEngine = reusableLocalKatago(); - installOutput(initialFailureEngine); - Lizzie.leelaz = initialFailureEngine; - AtomicInteger initialClosed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition initialAcquisition = - initialFailureEngine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> initialClosed.incrementAndGet()); - - assertTrue(dispatch(initialFailureEngine, "?800000000 cannot stop")); - assertEquals(1, initialClosed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.INITIAL_STOP_ERROR_RESPONSE), - initialAcquisition.lease().failureReason()); - assertFalse(initialFailureEngine.isLoaded()); - assertFalse(initialAcquisition.lease().isOwned()); - - Leelaz finalFailureEngine = reusableLocalKatago(); - ByteArrayOutputStream finalOutput = installOutput(finalFailureEngine); - Lizzie.leelaz = finalFailureEngine; - AtomicInteger finalClosed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition finalAcquisition = - finalFailureEngine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> finalClosed.incrementAndGet()); - processCommandResponse(finalFailureEngine, "=800000000"); - assertTrue(dispatch(finalFailureEngine, "")); - finalFailureEngine.sendCommand("version"); - - assertTrue(dispatch(finalFailureEngine, "?800000001 cannot stop")); - assertEquals(1, finalClosed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.FINAL_STOP_ERROR_RESPONSE), - finalAcquisition.lease().failureReason()); - assertFalse(finalFailureEngine.isLoaded()); - assertFalse(finalAcquisition.lease().isOwned()); - assertFalse(finalOutput.toString(StandardCharsets.UTF_8).contains("version\n")); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - } - } - - @Test - void staleIncarnationClosesOnlyTheOldTrackingLease() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Config previousConfig = Lizzie.config; - GtpConsolePane previousGtpConsole = Lizzie.gtpConsole; - Leelaz engine = reusableLocalKatago(); - installOutput(engine); - AtomicInteger closed = new AtomicInteger(); - try { - Lizzie.leelaz = engine; - Lizzie.frame = allocate(LizzieFrame.class); - Lizzie.config = allocate(Config.class); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatch(engine, "")); - - ByteArrayOutputStream currentOutput = new ByteArrayOutputStream(); - initializeStreams(engine, currentOutput); - assertFalse(acquisition.lease().send("kata-analyze B 10")); - - assertEquals(1, closed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.TRANSPORT_CLOSED), - acquisition.lease().failureReason()); - assertFalse(acquisition.lease().isOwned()); - assertTrue(engine.isLoaded()); - assertTrue(engine.isStarted()); - assertEquals("", currentOutput.toString(StandardCharsets.UTF_8)); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - } - } - - @Test - void staleRebindAfterActiveWriteClaimClosesTheOldLease() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - List lines = new ArrayList<>(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - lines::add, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - BlockingOutput oldOutput = new BlockingOutput(); - installOutput(state.engine, Leelaz.createCommandOutputStream(oldOutput)); - AtomicReference sendResult = new AtomicReference<>(); - Thread sendThread = - new Thread( - () -> sendResult.set(acquisition.lease().send("kata-analyze B 10")), - "tracking-send-before-rebind"); - sendThread.setDaemon(true); - - sendThread.start(); - assertTrue(oldOutput.writeStarted.await(1, TimeUnit.SECONDS)); - ByteArrayOutputStream currentOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, "=\n\n", currentOutput); - try { - state.engine.isNormalEnd = true; - invokeRead(state.engine); - assertEquals(List.of(), lines); - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals("", currentOutput.toString(StandardCharsets.UTF_8)); - } finally { - oldOutput.continueWrite.countDown(); - } - sendThread.join(1000L); - - assertFalse(sendThread.isAlive()); - assertFalse(sendResult.get()); - assertEquals(1, closed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.TRANSPORT_CLOSED), - acquisition.lease().failureReason()); - assertFalse(acquisition.lease().isOwned()); - assertTrue(state.engine.isLoaded()); - assertEquals("", currentOutput.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void rebindCallbackFailureCannotLeaveReaderAndCommandGatesClosed() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch failCallback = new CountDownLatch(1); - enqueueThrowingTrackedLoadSgf(state.engine, callbackStarted, failCallback); - ByteArrayOutputStream firstOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread firstRebind = - new Thread( - () -> { - try { - initializeStreams(state.engine, firstOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "tracking-rebind-with-callback-failure"); - firstRebind.setDaemon(true); - - firstRebind.start(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - try { - state.engine.sendCommand("version"); - } finally { - failCallback.countDown(); - } - firstRebind.join(1000L); - - assertFalse(firstRebind.isAlive()); - assertTrue(rebindFailure.get() instanceof java.lang.reflect.InvocationTargetException); - assertEquals( - "simulated rebind callback failure", rebindFailure.get().getCause().getMessage()); - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals("version\n", firstOutput.toString(StandardCharsets.UTF_8)); - - ByteArrayOutputStream secondOutput = new ByteArrayOutputStream(); - AtomicReference secondRebindFailure = new AtomicReference<>(); - Thread secondRebind = - new Thread( - () -> { - try { - initializeStreams(state.engine, secondOutput); - } catch (Throwable failure) { - secondRebindFailure.set(failure); - } - }, - "tracking-rebind-after-callback-failure"); - secondRebind.setDaemon(true); - secondRebind.start(); - secondRebind.join(1000L); - - assertFalse(secondRebind.isAlive()); - assertEquals(null, secondRebindFailure.get()); - } - } - - @Test - void stateResetCallbackRunsWithoutHoldingQueuedCommandMonitor() throws Exception { - Class failureHandlerType = - Class.forName("featurecat.lizzie.analysis.Leelaz$CommandSendFailureHandler"); - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch continueCallback = new CountDownLatch(1); - Object failureHandler = - java.lang.reflect.Proxy.newProxyInstance( - failureHandlerType.getClassLoader(), - new Class[] {failureHandlerType}, - (proxy, method, arguments) -> { - if (method.getName().equals("onStateResetAfterOutputWrite")) { - callbackStarted.countDown(); - if (!continueCallback.await(3, TimeUnit.SECONDS)) { - throw new IllegalStateException("timed out waiting to continue reset callback"); - } - } - return null; - }); - Class queuedCommandType = Class.forName("featurecat.lizzie.analysis.Leelaz$QueuedCommand"); - java.lang.reflect.Constructor constructor = - queuedCommandType.getDeclaredConstructor( - String.class, Runnable.class, failureHandlerType, boolean.class); - constructor.setAccessible(true); - Object queuedCommand = - constructor.newInstance("loadsgf /tmp/monitor.sgf", null, failureHandler, true); - Method markReset = - queuedCommandType.getDeclaredMethod( - "markStateResetAfterOutputWrite", RuntimeException.class); - markReset.setAccessible(true); - markReset.invoke(queuedCommand, new IllegalStateException("controlled reset")); - Method publishReset = queuedCommandType.getDeclaredMethod("publishStateResetAfterOutputWrite"); - publishReset.setAccessible(true); - AtomicReference publishFailure = new AtomicReference<>(); - Thread publishThread = - new Thread( - () -> { - try { - publishReset.invoke(queuedCommand); - } catch (Throwable failure) { - publishFailure.set(failure); - } - }, - "publish-loadsgf-state-reset"); - publishThread.setDaemon(true); - publishThread.start(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - CountDownLatch monitorAcquired = new CountDownLatch(1); - Thread monitorThread = - new Thread( - () -> { - synchronized (queuedCommand) { - monitorAcquired.countDown(); - } - }, - "acquire-queued-command-during-reset-callback"); - monitorThread.setDaemon(true); - monitorThread.start(); - try { - assertTrue(monitorAcquired.await(1, TimeUnit.SECONDS)); - } finally { - continueCallback.countDown(); - } - publishThread.join(1000L); - monitorThread.join(1000L); - - assertFalse(publishThread.isAlive()); - assertFalse(monitorThread.isAlive()); - assertEquals(null, publishFailure.get()); - } - - @ParameterizedTest - @EnumSource(SuccessfulTrackingClosePhase.class) - void successfulTrackingCloseCannotMoveOldQueueToConcurrentRebind( - SuccessfulTrackingClosePhase phase) throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - phase.prepareCloseBoundary(state.engine, acquisition.lease()); - state.engine.sendCommand("version"); - Object commandQueue = commandQueue(state.engine); - AtomicReference closeFailure = new AtomicReference<>(); - Thread closeThread = - new Thread( - () -> { - try { - dispatch(state.engine, ""); - } catch (Throwable failure) { - closeFailure.set(failure); - } - }, - "complete-successful-tracking-close"); - closeThread.setDaemon(true); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "rebind-after-successful-tracking-close"); - rebindThread.setDaemon(true); - boolean rebindWaitedForCutover; - - synchronized (commandQueue) { - closeThread.start(); - waitUntil(() -> !acquisition.lease().isOwned()); - rebindThread.start(); - waitUntil( - () -> - !rebindThread.isAlive() - || rebindThread.getState() == Thread.State.BLOCKED); - rebindWaitedForCutover = rebindThread.isAlive(); - } - closeThread.join(1000L); - rebindThread.join(1000L); - - assertTrue(rebindWaitedForCutover); - assertFalse(closeThread.isAlive()); - assertFalse(rebindThread.isAlive()); - assertEquals(null, closeFailure.get()); - assertEquals(null, rebindFailure.get()); - assertEquals(1, closed.get()); - assertEquals(java.util.Optional.empty(), acquisition.lease().failureReason()); - assertFalse(reboundOutput.toString(StandardCharsets.UTF_8).contains("version\n")); - } - } - - @ParameterizedTest - @EnumSource(TrackingTimeoutPhase.class) - void rebindWinnerMakesStartedTrackingTimeoutAStaleNoOp(TrackingTimeoutPhase phase) - throws Exception { - TimeoutLeelaz engine = reusableTimeoutKatago(); - phase.useShortTimeout(engine); - engine.blockTimeout = true; - try (TestState state = TestState.open(engine)) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - phase.startTimeout(state.engine, acquisition.lease()); - assertTrue(engine.timeoutStarted.await(1, TimeUnit.SECONDS)); - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch continueCallback = new CountDownLatch(1); - enqueueBlockingTrackedLoadSgf( - state.engine, callbackStarted, continueCallback, null); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "tracking-rebind-cutover"); - rebindThread.setDaemon(true); - rebindThread.start(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - state.engine.sendCommand("version"); - engine.continueTimeout.countDown(); - assertTrue(engine.timeoutFinished.await(1, TimeUnit.SECONDS)); - assertEquals(null, engine.timeoutFailure.get()); - continueCallback.countDown(); - rebindThread.join(1000L); - - assertFalse(rebindThread.isAlive()); - assertEquals(null, rebindFailure.get()); - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.TRANSPORT_CLOSED), - acquisition.lease().failureReason()); - assertTrue(state.engine.isLoaded()); - assertEquals("version\n", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @ParameterizedTest - @EnumSource(TrackingTimeoutPhase.class) - void timeoutWinnerOwnsCleanupAcrossTrackingRebind(TrackingTimeoutPhase phase) throws Exception { - TimeoutLeelaz engine = reusableTimeoutKatago(); - phase.useShortTimeout(engine); - engine.blockTimeout = true; - try (TestState state = TestState.open(engine)) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - phase.startTimeout(state.engine, acquisition.lease()); - assertTrue(engine.timeoutStarted.await(1, TimeUnit.SECONDS)); - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch continueCallback = new CountDownLatch(1); - enqueueBlockingTrackedLoadSgf( - state.engine, callbackStarted, continueCallback, null); - - engine.continueTimeout.countDown(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "tracking-rebind-after-timeout-claim"); - rebindThread.setDaemon(true); - rebindThread.start(); - waitUntil(() -> readerStreamRebindInProgress(state.engine)); - CountDownLatch rebindCallbackStarted = new CountDownLatch(1); - CountDownLatch continueRebindCallback = new CountDownLatch(1); - enqueueBlockingTrackedLoadSgf( - state.engine, rebindCallbackStarted, continueRebindCallback, null); - continueCallback.countDown(); - assertTrue(engine.timeoutFinished.await(1, TimeUnit.SECONDS)); - assertEquals(null, engine.timeoutFailure.get()); - assertTrue(rebindCallbackStarted.await(1, TimeUnit.SECONDS)); - state.engine.sendCommand("version"); - continueRebindCallback.countDown(); - rebindThread.join(1000L); - - assertFalse(rebindThread.isAlive()); - assertEquals(null, rebindFailure.get()); - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals( - java.util.Optional.of(phase.failure()), acquisition.lease().failureReason()); - assertEquals("version\n", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @ParameterizedTest - @EnumSource(TrackingTimeoutPhase.class) - void readerRebindRetiresCommandQueuedAfterTimeoutSnapshotBeforeGate( - TrackingTimeoutPhase phase) throws Exception { - TimeoutLeelaz engine = reusableTimeoutKatago(); - phase.useShortTimeout(engine); - engine.blockTimeout = true; - try (TestState state = TestState.open(engine)) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - phase.startTimeout(state.engine, acquisition.lease()); - assertTrue(engine.timeoutStarted.await(1, TimeUnit.SECONDS)); - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch continueCallback = new CountDownLatch(1); - enqueueBlockingTrackedLoadSgf( - state.engine, callbackStarted, continueCallback, null); - - engine.continueTimeout.countDown(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - state.engine.sendCommand("version"); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "tracking-rebind-after-timeout-snapshot"); - rebindThread.setDaemon(true); - rebindThread.start(); - waitUntil(() -> readerStreamRebindInProgress(state.engine)); - continueCallback.countDown(); - assertTrue(engine.timeoutFinished.await(1, TimeUnit.SECONDS)); - assertEquals(null, engine.timeoutFailure.get()); - rebindThread.join(1000L); - - assertFalse(rebindThread.isAlive()); - assertEquals(null, rebindFailure.get()); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @ParameterizedTest - @EnumSource(TrackingTimeoutPhase.class) - void readerRebindRetiresSentLoadSgfHandlerPreservedByTimeoutReset( - TrackingTimeoutPhase phase) throws Exception { - TimeoutLeelaz engine = reusableTimeoutKatago(); - phase.useShortTimeout(engine); - engine.blockTimeout = true; - try (TestState state = TestState.open(engine)) { - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch continueCallback = new CountDownLatch(1); - AtomicInteger stateResetCallbacks = new AtomicInteger(); - enqueueBlockingTrackedLoadSgf( - state.engine, callbackStarted, continueCallback, null, stateResetCallbacks); - assertEquals(1, pendingResponseHandlerCount(state.engine)); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - phase.startTimeoutAfterPendingLoadSgf(state.engine, acquisition.lease()); - assertTrue(engine.timeoutStarted.await(1, TimeUnit.SECONDS)); - - engine.continueTimeout.countDown(); - assertTrue(callbackStarted.await(1, TimeUnit.SECONDS)); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "tracking-rebind-after-timeout-preserved-handler"); - rebindThread.setDaemon(true); - rebindThread.start(); - waitUntil(() -> readerStreamRebindInProgress(state.engine)); - continueCallback.countDown(); - assertTrue(engine.timeoutFinished.await(1, TimeUnit.SECONDS)); - assertEquals(null, engine.timeoutFailure.get()); - rebindThread.join(1000L); - - assertFalse(rebindThread.isAlive()); - assertEquals(null, rebindFailure.get()); - assertEquals(0, pendingResponseHandlerCount(state.engine)); - assertEquals(1, stateResetCallbacks.get()); - processCommandResponse(state.engine, "=1"); - assertEquals(1, stateResetCallbacks.get()); - } - } - - @Test - void currentIncarnationTerminalClosesTrackingLeaseAsTransportFailure() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - Config previousConfig = Lizzie.config; - GtpConsolePane previousGtpConsole = Lizzie.gtpConsole; - Leelaz engine = reusableLocalKatago(); - installOutput(engine); - installInput(engine, ""); - AtomicInteger closed = new AtomicInteger(); - List dispositions = new ArrayList<>(); - RecordingHandoffTarget target = RecordingHandoffTarget.retained(); - try { - Lizzie.leelaz = engine; - Lizzie.frame = allocate(LizzieFrame.class); - Lizzie.config = allocate(Config.class); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - engine.isNormalEnd = true; - Leelaz.TrackingStreamLeaseAcquisition acquisition = - engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet(), dispositions::add); - Leelaz.TrackingHandoffClaim claim = engine.claimTrackingHandoff(target); - - invokeRead(engine); - - assertEquals(1, closed.get()); - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(0, target.activations.get()); - assertEquals(1, target.failures.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.TRANSPORT_CLOSED), - acquisition.lease().failureReason()); - assertFalse(acquisition.lease().isOwned()); - assertFalse(engine.isStarted()); - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), dispositions); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, acquisition.lease().disposition()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - } - } - - @Test - void releaseWhileAcquiringClosesAfterInitialFenceWithoutBecomingReady() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger ready = new AtomicInteger(); - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> ready.incrementAndGet(), lease -> closed.incrementAndGet()); - - assertTrue(acquisition.lease().release()); - assertFalse(acquisition.lease().release()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(0, ready.get()); - assertEquals(1, closed.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - assertTrue(state.engine.isLoaded()); - } - } - - @Test - void acquiringTrackingLeaseAcceptsOneTypedHandoffAndActivatesAfterInitialFence() - throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingHandoffTarget target = RecordingHandoffTarget.retained(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - Leelaz.TrackingHandoffClaim second = - state.engine.claimTrackingHandoff(RecordingHandoffTarget.foreground()); - - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertEquals(Leelaz.TrackingHandoffState.ACCEPTED_PENDING, claim.state()); - assertEquals(Leelaz.TrackingHandoffAvailability.BUSY, second.availability()); - assertEquals(0, target.activations.get()); - assertFalse(acquisition.lease().send("kata-analyze B 10")); - - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(1, target.activations.get()); - assertEquals(0, target.failures.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void foregroundHandoffOwnsExclusiveGateAfterInitialFence() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingHandoffTarget target = RecordingHandoffTarget.foreground(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - state.engine.sendCommand("version"); - - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertEquals(1, target.activations.get()); - assertTrue(state.engine.hasExclusiveGtpLeaseOwnedBy(target)); - assertFalse(state.engine.beginExclusiveGtpLifecycleTransition()); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void lifecycleReservationClaimsTrackingAndRejectsOrdinaryCommandBehindQueueGate() - throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - List dispositions = new ArrayList<>(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> {}, dispositions::add); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - - Leelaz.ExclusiveGtpLifecycleReservation reservation = - state.engine.beginExclusiveGtpLifecycleReservation(); - state.engine.sendCommand("stop"); - - assertTrue(reservation != null, "destructive lifecycle must win active tracking once"); - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), dispositions); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - state.output.toString(StandardCharsets.UTF_8)); - assertEquals(null, state.engine.beginExclusiveGtpLifecycleReservation()); - - assertTrue(dispatch(state.engine, "")); - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - assertFalse(state.output.toString(StandardCharsets.UTF_8).endsWith("stop\nstop\n")); - - reservation.close(); - - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - state.output.toString(StandardCharsets.UTF_8), - "an uncredentialed ordinary command rejected behind the lifecycle gate must not replay"); - } - } - - @Test - void typedHandoffClearsReleaseDispositionBeforeActivationDespiteObserverFailure() - throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - List dispositions = new ArrayList<>(); - AtomicInteger activations = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, - lease -> {}, - lease -> {}, - disposition -> { - dispositions.add(disposition); - throw new IllegalStateException("simulated disposition observer failure"); - }); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), dispositions); - activations.incrementAndGet(); - assertTrue(activation.completeRetainedEngineMode()); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) {} - }; - - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, acquisition.lease().disposition()); - assertEquals(1, activations.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - } - } - - @Test - void activeTrackingHandoffKeepsOrdinaryQueueClosedUntilRetainedTargetCompletes() - throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - BlockingHandoffTarget target = new BlockingHandoffTarget(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - state.engine.sendCommand("version"); - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n800000002 stop\n", - state.output.toString(StandardCharsets.UTF_8)); - - assertTrue(dispatch(state.engine, "")); - assertTrue(dispatch(state.engine, "=800000002")); - AtomicReference fenceFailure = new AtomicReference<>(); - Thread fenceThread = - new Thread( - () -> { - try { - dispatch(state.engine, ""); - } catch (Throwable failure) { - fenceFailure.set(failure); - } - }, - "tracking-handoff-final-fence"); - fenceThread.setDaemon(true); - fenceThread.start(); - assertTrue(target.activationStarted.await(1, TimeUnit.SECONDS)); - - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("version\n")); - assertEquals( - Leelaz.TrackingHandoffAvailability.BUSY, - state.engine.claimTrackingHandoff(RecordingHandoffTarget.foreground()).availability()); - assertFalse(state.engine.beginExclusiveGtpLifecycleTransition()); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE, - state.engine.previewForegroundAnalysisLeaseAvailability()); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.EXISTING_LEASE, - state - .engine - .acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}) - .availability()); - - target.allowCompletion.countDown(); - fenceThread.join(1000L); - - assertFalse(fenceThread.isAlive()); - assertEquals(null, fenceFailure.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - } - } - - @Test - void readerRebindFailsPendingHandoffOnceBeforeReopeningOrdinaryQueue() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - List dispositions = new ArrayList<>(); - RecordingHandoffTarget target = RecordingHandoffTarget.retained(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> {}, dispositions::add); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - - initializeStreams(state.engine, reboundOutput); - state.engine.sendCommand("version"); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(0, target.activations.get()); - assertEquals(1, target.failures.get()); - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), dispositions); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, acquisition.lease().disposition()); - assertEquals("version\n", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void readerRebindWinsAgainstInFlightHandoffActivation() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - BlockingHandoffTarget target = - new BlockingHandoffTarget(() -> state.engine.sendCommand("name"), true); - AtomicReference fenceFailure = new AtomicReference<>(); - AtomicReference rebindFailure = new AtomicReference<>(); - CountDownLatch rebindFinished = new CountDownLatch(1); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - assertTrue(dispatch(state.engine, "")); - assertTrue(dispatch(state.engine, "=800000002")); - Thread fenceThread = - new Thread( - () -> { - try { - dispatch(state.engine, ""); - } catch (Throwable failure) { - fenceFailure.set(failure); - } - }, - "tracking-handoff-rebind-during-activation"); - fenceThread.setDaemon(true); - fenceThread.start(); - assertTrue(target.activationStarted.await(1, TimeUnit.SECONDS)); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } finally { - rebindFinished.countDown(); - } - }, - "tracking-handoff-rebind-cutover"); - rebindThread.setDaemon(true); - - rebindThread.start(); - waitUntil(() -> readerStreamRebindInProgress(state.engine)); - state.engine.sendCommand("version"); - - assertEquals(0, target.failures.get()); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - - target.allowCompletion.countDown(); - assertTrue(target.failureStarted.await(1, TimeUnit.SECONDS)); - notifyEngineArbitrationWaiters(state.engine); - assertFalse(rebindFinished.await(100, TimeUnit.MILLISECONDS)); - assertEquals(0, target.failures.get()); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - - target.allowFailureCompletion.countDown(); - fenceThread.join(1000L); - rebindThread.join(1000L); - state.engine.sendCommand("version"); - - assertFalse(fenceThread.isAlive()); - assertFalse(rebindThread.isAlive()); - assertEquals(null, fenceFailure.get()); - assertEquals(null, rebindFailure.get()); - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(1, target.failures.get()); - assertEquals("version\n", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void targetCancellationSettlesOnceAndCallbackFailureCannotLoseQueueWakeup() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger activations = new AtomicInteger(); - AtomicInteger failures = new AtomicInteger(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - activations.incrementAndGet(); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failures.incrementAndGet(); - throw new IllegalStateException("simulated target failure callback exception"); - } - }; - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - - assertTrue(claim.cancel()); - assertFalse(claim.cancel()); - state.engine.sendCommand("version"); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(0, activations.get()); - assertEquals(1, failures.get()); - assertFalse(acquisition.lease().isOwned()); - assertEquals("800000000 stop\nversion\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void retainedHandoffCannotActivateAsForegroundOwner() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicReference mismatchedActivation = new AtomicReference<>(); - AtomicReference failure = new AtomicReference<>(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - mismatchedActivation.set(activation.activateForegroundAnalysis(line -> {}, () -> {})); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure reason) { - failure.compareAndSet(null, reason); - } - }; - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - state.engine.sendCommand("version"); - - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(false, mismatchedActivation.get()); - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(Leelaz.TrackingHandoffFailure.ACTIVATION_FAILED, failure.get()); - assertFalse(state.engine.hasExclusiveGtpLeaseOwnedBy(target)); - assertEquals("800000000 stop\nversion\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void handoffKindIsCapturedOnceAtClaim() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger kindReads = new AtomicInteger(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return kindReads.incrementAndGet() == 1 - ? Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE - : Leelaz.TrackingHandoffKind.FOREGROUND_ANALYSIS; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - assertTrue(activation.completeRetainedEngineMode()); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) {} - }; - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(1, kindReads.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - } - } - - @Test - void targetValidationExceptionFailsHandoffAndReopensOrdinaryQueue() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger failures = new AtomicInteger(); - AtomicReference reason = new AtomicReference<>(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - throw new IllegalStateException("simulated target validation failure"); - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - throw new AssertionError("invalid target must not activate"); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failures.incrementAndGet(); - reason.compareAndSet(null, failure); - } - }; - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - state.engine.sendCommand("version"); - - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(1, failures.get()); - assertEquals(Leelaz.TrackingHandoffFailure.ACTIVATION_FAILED, reason.get()); - assertEquals("800000000 stop\nversion\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void trackingClosedCallbackFailureCannotBlockHandoffActivationOrQueueWakeup() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - RecordingHandoffTarget target = RecordingHandoffTarget.retained(); - state.engine.acquireTrackingStreamLease( - line -> {}, - lease -> {}, - lease -> { - closed.incrementAndGet(); - throw new IllegalStateException("simulated tracking closed callback failure"); - }); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - state.engine.sendCommand("version"); - - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertEquals(1, closed.get()); - assertEquals(1, target.activations.get()); - assertEquals(0, target.failures.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertEquals("800000000 stop\nversion\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void inFlightActivationWinsAgainstLateCancellation() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - CountDownLatch activationStarted = new CountDownLatch(1); - CountDownLatch continueActivation = new CountDownLatch(1); - AtomicReference completion = new AtomicReference<>(); - AtomicInteger failures = new AtomicInteger(); - AtomicReference activationFailure = new AtomicReference<>(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - activationStarted.countDown(); - try { - assertTrue(continueActivation.await(1, TimeUnit.SECONDS)); - completion.set(activation.completeRetainedEngineMode()); - } catch (Throwable failure) { - activationFailure.set(failure); - } - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failures.incrementAndGet(); - } - }; - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - state.engine.sendCommand("version"); - assertTrue(dispatch(state.engine, "")); - assertTrue(dispatch(state.engine, "=800000002")); - Thread fenceThread = - new Thread( - () -> { - try { - dispatch(state.engine, ""); - } catch (Throwable failure) { - activationFailure.compareAndSet(null, failure); - } - }, - "tracking-handoff-cancel-during-activation"); - fenceThread.setDaemon(true); - - fenceThread.start(); - assertTrue(activationStarted.await(1, TimeUnit.SECONDS)); - assertFalse(claim.cancel()); - - assertEquals(0, failures.get()); - assertFalse(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - - continueActivation.countDown(); - fenceThread.join(1000L); - - assertFalse(fenceThread.isAlive()); - assertEquals(null, activationFailure.get()); - assertEquals(true, completion.get()); - assertEquals(0, failures.get()); - assertEquals(Leelaz.TrackingHandoffState.ACTIVE, claim.state()); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - } - } - - @Test - void releaseRequestedTrackingLeaseRejectsLateHandoff() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - assertTrue(acquisition.lease().release()); - - Leelaz.TrackingHandoffClaim claim = - state.engine.claimTrackingHandoff(RecordingHandoffTarget.retained()); - - assertEquals(Leelaz.TrackingHandoffAvailability.BUSY, claim.availability()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - } - } - - @Test - void ordinaryQueueWinnerMakesLaterTypedHandoffBusy() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - state.engine.sendCommand("version"); - - Leelaz.TrackingHandoffClaim claim = - state.engine.claimTrackingHandoff(RecordingHandoffTarget.retained()); - - assertEquals(Leelaz.TrackingHandoffAvailability.BUSY, claim.availability()); - assertFalse(acquisition.lease().release()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - } - } - - @Test - void ordinaryCommandClaimsTrackingReleaseAfterQueueAdmission() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommand("komi 7.5"); - - assertEquals(List.of(Leelaz.TrackingReleaseReason.ORDINARY_OPERATION), observer.reasons); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, acquisition.lease().disposition()); - assertEquals( - Leelaz.TrackingHandoffAvailability.BUSY, - state.engine.claimTrackingHandoff(RecordingHandoffTarget.retained()).availability()); - assertEquals( - "800000000 stop\n800000001 stop\n", state.output.toString(StandardCharsets.UTF_8)); - - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("komi 7.5\n")); - } - } - - @Test - void safeRawQueryFreezesThenOrdinaryCommandClearsBeforeFinalFence() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(state.engine.sendRawConsoleCommand("KnOwN_CoMmAnD showboard")); - state.engine.sendCommand("komi 6.5"); - - assertEquals( - List.of( - Leelaz.TrackingReleaseReason.SAFE_READ_ONLY_QUERY, - Leelaz.TrackingReleaseReason.ORDINARY_OPERATION), - observer.reasons); - assertEquals( - List.of( - Leelaz.TrackingReleaseDisposition.FROZEN_BY_SAFE, - Leelaz.TrackingReleaseDisposition.CLEARED), - observer.dispositions); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, acquisition.lease().disposition()); - assertEquals( - "800000000 stop\n800000001 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void rawConsoleWhitelistRejectsIdsWrongArityAndUnsafeCommandsBeforeRelease() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - for (String command : - List.of( - "1 name", - "name extra", - "known_command", - "known_command showboard extra", - "kata-get-rules", - "kata-raw-nn 0", - "unknown_command")) { - assertFalse(state.engine.sendRawConsoleCommand(command), command); - } - - assertEquals(List.of(), observer.reasons); - assertEquals(Leelaz.TrackingReleaseDisposition.ACTIVE, acquisition.lease().disposition()); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void rawConsoleWhitelistAcceptsEveryStrictSafeQueryCaseInsensitively() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - for (String command : - List.of( - "NaMe", - "VERSION", - "Protocol_Version", - "LIST_COMMANDS", - "known_COMMAND kata-analyze", - "ShowBoard")) { - assertTrue(state.engine.sendRawConsoleCommand(command), command); - } - - assertEquals(List.of(Leelaz.TrackingReleaseReason.SAFE_READ_ONLY_QUERY), observer.reasons); - assertEquals( - List.of(Leelaz.TrackingReleaseDisposition.FROZEN_BY_SAFE), observer.dispositions); - assertEquals( - "800000000 stop\n800000001 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void ordinaryThenSafeCannotDowngradeDispositionAndClosedCommandsDoNotNotify() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommand("komi 7.5"); - assertTrue(state.engine.sendRawConsoleCommand("name")); - assertEquals(List.of(Leelaz.TrackingReleaseReason.ORDINARY_OPERATION), observer.reasons); - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), observer.dispositions); - - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - state.engine.sendCommand("version"); - - assertEquals(List.of(Leelaz.TrackingReleaseReason.ORDINARY_OPERATION), observer.reasons); - assertEquals(List.of(Leelaz.TrackingReleaseDisposition.CLEARED), observer.dispositions); - } - } - - @Test - void secondOrdinaryEnqueuePathClaimsReleaseAndSharesCoalescingCleanup() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommandNoLeelaz2("version"); - - assertEquals(List.of(Leelaz.TrackingReleaseReason.ORDINARY_OPERATION), observer.reasons); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - } - } - - @Test - void trackingFaultEndsBlockingLoadSgfOnceWithoutWritingLoadCommand() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - Path sgf = Files.createTempFile("tracking-fault-loadsgf-", ".sgf"); - AtomicInteger consumed = new AtomicInteger(); - AtomicReference failure = new AtomicReference<>(); - Thread loadThread = - new Thread( - () -> { - try { - state.engine.loadSgf(sgf, consumed::incrementAndGet); - } catch (Throwable thrown) { - failure.set(thrown); - } - }, - "tracking-fault-blocking-loadsgf"); - loadThread.setDaemon(true); - try { - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - loadThread.start(); - waitUntil(() -> commandQueueSize(state.engine) == 1); - - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, reboundOutput); - loadThread.join(1000L); - - assertFalse(loadThread.isAlive()); - assertEquals(1, consumed.get()); - assertTrue(failure.get() instanceof RuntimeException); - assertFalse( - state.output.toString(StandardCharsets.UTF_8).contains("loadsgf "), - state.output.toString(StandardCharsets.UTF_8)); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - } finally { - Files.deleteIfExists(sgf); - } - } - } - - - @Test - void komiQueuesBehindOrdinaryTrackingReleaseWithoutReportingBusy() throws Exception { - Board previousBoard = Lizzie.board; - FeedbackRecordingLeelaz engine = new FeedbackRecordingLeelaz(); - configureLocalKatago(engine); - try (TestState state = TestState.open(engine)) { - Lizzie.board = new Board(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommand("clear_board"); - state.engine.komi(7.5); - - assertEquals(0, engine.feedbackCount.get()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - List commands = outputCommandPayloads(state.output); - assertEquals( - List.of("clear_board", "komi 7.5"), - commands.subList(commands.size() - 2, commands.size()), - commands.toString()); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void textKomiQueuesBehindOrdinaryTrackingReleaseWithoutReportingBusy() throws Exception { - Board previousBoard = Lizzie.board; - FeedbackRecordingLeelaz engine = new FeedbackRecordingLeelaz(); - configureLocalKatago(engine); - try (TestState state = TestState.open(engine)) { - Lizzie.board = new Board(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommand("clear_board"); - state.engine.komiNoMenu(7.5); - - assertEquals(0, engine.feedbackCount.get()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - List commands = outputCommandPayloads(state.output); - assertEquals( - List.of("clear_board", "komi 7.5"), - commands.subList(commands.size() - 2, commands.size()), - commands.toString()); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void boardSizeQueuesBehindOrdinaryTrackingReleaseWithoutReportingBusy() throws Exception { - Board previousBoard = Lizzie.board; - FeedbackRecordingLeelaz engine = new FeedbackRecordingLeelaz(); - configureLocalKatago(engine); - try (TestState state = TestState.open(engine)) { - Lizzie.board = - new Board() { - @Override - public void reopen(int width, int height) {} - }; - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - state.engine.sendCommand("clear_board"); - state.engine.boardSize(13, 13); - - assertEquals(0, engine.feedbackCount.get()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - List commands = outputCommandPayloads(state.output); - assertEquals( - List.of("clear_board", "boardsize 13"), - commands.subList(commands.size() - 2, commands.size()), - commands.toString()); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void boardSizeMirrorsBeforeRealBoardReopenCommands() throws Exception { - Board previousBoard = Lizzie.board; - int previousBoardWidth = Board.boardWidth; - int previousBoardHeight = Board.boardHeight; - Leelaz previousSecondEngine = Lizzie.leelaz2; - WinrateGraph previousWinrateGraph = LizzieFrame.winrateGraph; - try (TestState state = TestState.open(reusableLocalKatago())) { - Leelaz secondEngine = reusableLocalKatago(); - ByteArrayOutputStream secondOutput = installOutput(secondEngine); - Lizzie.frame = allocate(PonderTrackingFrame.class); - LizzieFrame.winrateGraph = allocate(WinrateGraph.class); - Board.boardWidth = 19; - Board.boardHeight = 19; - Zobrist.init(); - Lizzie.board = new Board(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - Lizzie.config.extraMode = ExtraMode.Double_Engine; - Lizzie.leelaz2 = secondEngine; - - state.engine.boardSize(13, 13); - - List mirroredCommands = outputCommandPayloads(secondOutput); - assertTrue(mirroredCommands.size() >= 2, mirroredCommands.toString()); - assertEquals( - List.of("boardsize 13", "clear_board"), - mirroredCommands.subList(0, 2), - mirroredCommands.toString()); - } finally { - Board.boardWidth = previousBoardWidth; - Board.boardHeight = previousBoardHeight; - Zobrist.init(); - Lizzie.board = previousBoard; - Lizzie.leelaz2 = previousSecondEngine; - LizzieFrame.winrateGraph = previousWinrateGraph; - } - } - - @Test - void boardSizeRejectsBeforeReopenWhenMirrorLifecycleOwnsAdmission() throws Exception { - Board previousBoard = Lizzie.board; - int previousBoardWidth = Board.boardWidth; - int previousBoardHeight = Board.boardHeight; - Leelaz previousSecondEngine = Lizzie.leelaz2; - WinrateGraph previousWinrateGraph = LizzieFrame.winrateGraph; - try (TestState state = TestState.open(reusableLocalKatago())) { - PonderTrackingFrame frame = allocate(PonderTrackingFrame.class); - Leelaz secondEngine = reusableLocalKatago(); - ByteArrayOutputStream secondOutput = installOutput(secondEngine); - Lizzie.frame = frame; - LizzieFrame.winrateGraph = allocate(WinrateGraph.class); - Board.boardWidth = 19; - Board.boardHeight = 19; - Zobrist.init(); - Lizzie.board = new Board(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - int originalEngineWidth = state.engine.width; - int originalEngineHeight = state.engine.height; - try (HeldLifecycleReservation ignored = HeldLifecycleReservation.open(secondEngine)) { - Lizzie.config.extraMode = ExtraMode.Double_Engine; - Lizzie.leelaz2 = secondEngine; - - state.engine.boardSize(13, 13); - - assertEquals(19, Board.boardWidth); - assertEquals(19, Board.boardHeight); - assertEquals(originalEngineWidth, state.engine.width); - assertEquals(originalEngineHeight, state.engine.height); - assertEquals(0, frame.redrawCount); - assertEquals(0, frame.refreshCount); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("boardsize 13\n")); - } - assertTrue(acquisition.lease().release()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertEquals(19, Board.boardWidth); - assertEquals(19, Board.boardHeight); - assertEquals(originalEngineWidth, state.engine.width); - assertEquals(originalEngineHeight, state.engine.height); - assertEquals(0, frame.redrawCount); - assertEquals(0, frame.refreshCount); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("boardsize 13\n")); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - } finally { - Board.boardWidth = previousBoardWidth; - Board.boardHeight = previousBoardHeight; - Zobrist.init(); - Lizzie.board = previousBoard; - Lizzie.leelaz2 = previousSecondEngine; - LizzieFrame.winrateGraph = previousWinrateGraph; - } - } - - @Test - void lifecycleWinnerKeepsKomiAndBoardSizeBusyWhileTrackingSettles() throws Exception { - Board previousBoard = Lizzie.board; - FeedbackRecordingLeelaz engine = new FeedbackRecordingLeelaz(); - configureLocalKatago(engine); - try (TestState state = TestState.open(engine)) { - Lizzie.board = - new Board() { - @Override - public void reopen(int width, int height) {} - }; - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - float originalKomi = state.engine.komi; - int originalWidth = state.engine.width; - int originalHeight = state.engine.height; - - Leelaz.ExclusiveGtpLifecycleReservation reservation = - state.engine.beginExclusiveGtpLifecycleReservation(); - assertTrue(reservation != null); - try { - state.engine.komi(5.5); - state.engine.komiNoMenu(6.5); - state.engine.boardSize(13, 13); - } finally { - reservation.close(); - } - - assertEquals(3, engine.feedbackCount.get()); - assertEquals(originalKomi, state.engine.komi); - assertEquals(originalWidth, state.engine.width); - assertEquals(originalHeight, state.engine.height); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("komi 5.5\n")); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("komi 6.5\n")); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("boardsize 13\n")); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void statefulPublicEntriesDoNotPublishWhenMirrorRejectsAtomicAdmission() throws Exception { - Board previousBoard = Lizzie.board; - Leelaz previousSecondEngine = Lizzie.leelaz2; - try (TestState state = TestState.open(reusableLocalKatago())) { - Lizzie.board = - new Board() { - @Override - public void reopen(int width, int height) {} - }; - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - Leelaz secondEngine = reusableLocalKatago(); - ByteArrayOutputStream secondOutput = installOutput(secondEngine); - float originalKomi = state.engine.komi; - double originalGameKomi = Lizzie.board.getHistory().getGameInfo().getKomi(); - int originalWidth = state.engine.width; - int originalHeight = state.engine.height; - - try (HeldLifecycleReservation ignored = HeldLifecycleReservation.open(secondEngine)) { - Lizzie.config.extraMode = ExtraMode.Double_Engine; - Lizzie.leelaz2 = secondEngine; - state.engine.komi(5.5); - state.engine.komiNoMenu(6.5); - state.engine.boardSize(13, 13); - - assertEquals(originalKomi, state.engine.komi); - assertEquals(originalGameKomi, Lizzie.board.getHistory().getGameInfo().getKomi()); - assertEquals(originalWidth, state.engine.width); - assertEquals(originalHeight, state.engine.height); - String primaryCommands = state.output.toString(StandardCharsets.UTF_8); - assertFalse(primaryCommands.contains("komi 5.5\n")); - assertFalse(primaryCommands.contains("komi 6.5\n")); - assertFalse(primaryCommands.contains("boardsize 13\n")); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - } - assertTrue(acquisition.lease().release()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertEquals(originalKomi, state.engine.komi); - assertEquals(originalGameKomi, Lizzie.board.getHistory().getGameInfo().getKomi()); - assertEquals(originalWidth, state.engine.width); - assertEquals(originalHeight, state.engine.height); - String primaryCommands = state.output.toString(StandardCharsets.UTF_8); - assertFalse(primaryCommands.contains("komi 5.5\n")); - assertFalse(primaryCommands.contains("komi 6.5\n")); - assertFalse(primaryCommands.contains("boardsize 13\n")); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - } finally { - Lizzie.board = previousBoard; - Lizzie.leelaz2 = previousSecondEngine; - } - } - - private static List rawOutputCommands(ByteArrayOutputStream output) { - List commands = new ArrayList<>(); - for (String line : output.toString(StandardCharsets.UTF_8).split("\\R")) { - String command = line.trim(); - if (!command.isEmpty()) { - commands.add(command); - } - } - return commands; - } - - private static String commandPayload(String command) { - int separator = command.indexOf(' '); - if (separator > 0 && command.substring(0, separator).chars().allMatch(Character::isDigit)) { - return command.substring(separator + 1); - } - return command; - } - - private static List outputCommandPayloads(ByteArrayOutputStream output) { - List commands = new ArrayList<>(); - for (String command : rawOutputCommands(output)) { - commands.add(commandPayload(command)); - } - return commands; - } - - private static void acknowledgeCompoundPositionRestore(TestState state, Thread restore) - throws Exception { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3); - int inspected = 0; - boolean finalFenceAcknowledged = false; - while (restore.isAlive() && System.nanoTime() < deadline) { - List commands = rawOutputCommands(state.output); - while (inspected < commands.size()) { - String command = commands.get(inspected++); - int separator = command.indexOf(' '); - if (separator <= 0 - || !command.substring(0, separator).chars().allMatch(Character::isDigit)) { - continue; - } - long commandId = Long.parseLong(command.substring(0, separator)); - if (commandId < 900_000_000L) { - continue; - } - String payload = command.substring(separator + 1); - assertFalse(payload.contains("analyze"), "analysis must wait for the position ACK"); - processCommandResponse(state.engine, "=" + commandId); - if (payload.equals("name")) { - finalFenceAcknowledged = true; - } - } - if (restore.isAlive()) { - Thread.sleep(10L); - } - } - restore.join(1_000L); - assertFalse(restore.isAlive(), "compound position restore must finish after its ACKs"); - assertTrue(finalFenceAcknowledged, "compound position restore must finish through final name"); - } - - private static void acknowledgeLastPositionCommand(TestState state) throws Exception { - String commands = state.output.toString(StandardCharsets.UTF_8).trim(); - String last = commands.substring(commands.lastIndexOf('\n') + 1); - String first = last.split(" ", 2)[0]; - assertFalse(last.contains("analyze"), "analysis must wait for the position ACK"); - processCommandResponse(state.engine, "=" + (Character.isDigit(first.charAt(0)) ? first : "")); - } - - @Test - void navigationAfterTrackingQueuesPonderForTheNewPosition() throws Exception { - Board previousBoard = Lizzie.board; - try (TestState state = TestState.open(reusableLocalKatago())) { - Lizzie.board = new Board(); - Lizzie.frame = allocate(PonderTrackingFrame.class); - Lizzie.config.analyzeBlack = true; - Lizzie.config.analyzeWhite = true; - Lizzie.config.analyzeUpdateIntervalCentisec = 10; - state.engine.Pondering(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze 10 allow B D4 1 allow W D4 1")); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "info move D4 visits 1")); - assertTrue(dispatch(state.engine, "")); - - state.engine.playMove(Stone.BLACK, "D4", true, false); - - assertTrue(state.engine.isPondering()); - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - acknowledgeLastPositionCommand(state); - String output = state.output.toString(StandardCharsets.UTF_8); - assertTrue(output.lastIndexOf("kata-analyze") > output.lastIndexOf("play B D4"), output); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void backwardNavigationAfterTrackingQueuesPonderForTheNewPosition() throws Exception { - Board previousBoard = Lizzie.board; - try (TestState state = TestState.open(reusableLocalKatago())) { - Lizzie.board = new Board(); - Lizzie.frame = allocate(PonderTrackingFrame.class); - Lizzie.config.analyzeBlack = true; - Lizzie.config.analyzeWhite = true; - Lizzie.config.analyzeUpdateIntervalCentisec = 10; - state.engine.Pondering(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze 10 allow B D4 1 allow W D4 1")); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - - state.engine.undo(true, false); - - assertTrue(state.engine.isPondering()); - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - acknowledgeLastPositionCommand(state); - String output = state.output.toString(StandardCharsets.UTF_8); - assertTrue(output.lastIndexOf("kata-analyze") > output.lastIndexOf("undo"), output); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void positionRestoreAfterTrackingQueuesPonderForTheRestoredPosition() throws Exception { - Board previousBoard = Lizzie.board; - try (TestState state = TestState.open(reusableLocalKatago())) { - Lizzie.board = new Board(); - Lizzie.frame = allocate(PonderTrackingFrame.class); - Lizzie.config.analyzeBlack = true; - Lizzie.config.analyzeWhite = true; - Lizzie.config.analyzeUpdateIntervalCentisec = 10; - state.engine.Pondering(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze 10 allow B D4 1 allow W D4 1")); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - - AtomicReference restoreFailure = new AtomicReference<>(); - Thread restore = - new Thread( - () -> { - try { - Lizzie.board.resendMoveToEngine(state.engine, false); - } catch (Throwable failure) { - restoreFailure.set(failure); - } - }, - "tracking-position-restore"); - restore.start(); - waitUntil( - () -> - rawOutputCommands(state.output).stream() - .anyMatch(command -> command.equals("800000002 stop"))); - assertTrue(restore.isAlive(), "restore must wait for the tracking final stop"); - - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - acknowledgeCompoundPositionRestore(state, restore); - assertEquals(null, restoreFailure.get()); - assertTrue(state.engine.isPondering()); - waitUntil( - () -> - outputCommandPayloads(state.output).stream() - .anyMatch(command -> command.startsWith("kata-analyze"))); - List commands = outputCommandPayloads(state.output); - int finalFence = commands.lastIndexOf("name"); - int analysis = -1; - for (int index = 0; index < commands.size(); index++) { - if (commands.get(index).startsWith("kata-analyze")) { - analysis = index; - } - } - assertTrue(finalFence >= 0, commands.toString()); - assertTrue(analysis > finalFence, commands.toString()); - } finally { - Lizzie.board = previousBoard; - } - } - - @Test - void pendingTypedHandoffRejectsStatefulOrdinaryRequestsWithoutStateOrBytes() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = - state.engine.claimTrackingHandoff(RecordingHandoffTarget.retained()); - - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertFalse(state.engine.genmove("B", true)); - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void manualGenmoveSetsAndClearsRequestStateAtWriterBoundary() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(state.engine.genmove("B", true)); - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - - assertTrue(dispatch(state.engine, "=800000001")); - installOutput( - state.engine, - Leelaz.createCommandOutputStream( - new OutputStream() { - @Override - public void write(int value) throws IOException { - throw new IOException("simulated manual genmove write failure"); - } - })); - assertTrue(dispatch(state.engine, "")); - - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - assertFalse(acquisition.lease().isOwned()); - assertEquals(List.of(List.of(false, true), List.of(false, false)), state.menu.transitions); - } - } - - @Test - void acceptedManualGenmoveDoesNotReportExistingLeaseBusy() throws Exception { - FeedbackRecordingLeelaz engine = new FeedbackRecordingLeelaz(); - configureLocalKatago(engine); - try (TestState state = TestState.open(engine)) { - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(state.engine.genmove("B", true)); - - assertEquals(0, engine.feedbackCount.get()); - } - } - - @Test - void mirrorAdmissionRejectionLeavesNeitherOrdinaryQueue() throws Exception { - Leelaz previousSecondEngine = Lizzie.leelaz2; - try (TestState state = TestState.open(reusableLocalKatago())) { - RecordingDispositionObserver observer = new RecordingDispositionObserver(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}, observer); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - Leelaz secondEngine = reusableLocalKatago(); - ByteArrayOutputStream secondOutput = installOutput(secondEngine); - try (HeldLifecycleReservation ignored = HeldLifecycleReservation.open(secondEngine)) { - Lizzie.config.extraMode = ExtraMode.Double_Engine; - Lizzie.leelaz2 = secondEngine; - - state.engine.sendCommand("komi 7.5"); - - assertTrue(observer.reasons.isEmpty()); - assertEquals("800000000 stop\n", state.output.toString(StandardCharsets.UTF_8)); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - } - assertTrue(acquisition.lease().release()); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("komi 7.5\n")); - assertEquals("", secondOutput.toString(StandardCharsets.UTF_8)); - } finally { - Lizzie.leelaz2 = previousSecondEngine; - } - } - - @Test - void preWriteRebindCancelsStatefulRequestsWithoutArmingState() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(state.engine.genmove("B", true)); - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, reboundOutput); - - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - assertEquals(List.of(), state.menu.transitions); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void postWriteRebindCleansManualGenmoveStateExactlyOnce() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - ensureReaderStreamBinding(state.engine); - - assertTrue(state.engine.genmove("B", true)); - assertTrue(state.engine.isInputCommand); - assertTrue(state.engine.isThinking); - - ByteArrayOutputStream secondReboundOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, secondReboundOutput); - assertFalse(state.engine.isInputCommand); - assertFalse(state.engine.isThinking); - assertEquals(List.of(List.of(false, true), List.of(false, false)), state.menu.transitions); - - ByteArrayOutputStream thirdReboundOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, thirdReboundOutput); - assertEquals(List.of(List.of(false, true), List.of(false, false)), state.menu.transitions); - } - } - - - @Test - void trackingFenceTimeoutsFailClosedAndReportOnce() throws Exception { - TimeoutLeelaz initialEngine = reusableTimeoutKatago(); - initialEngine.initialStopTimeoutMillis = 25L; - try (TestState state = TestState.open(initialEngine)) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - - waitUntil(() -> closed.get() == 1); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.INITIAL_STOP_TIMEOUT), - acquisition.lease().failureReason()); - assertFalse(state.engine.isLoaded()); - } - - TimeoutLeelaz finalEngine = reusableTimeoutKatago(); - finalEngine.releaseStopTimeoutMillis = 25L; - try (TestState state = TestState.open(finalEngine)) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().release()); - - waitUntil(() -> closed.get() == 1); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.FINAL_STOP_TIMEOUT), - acquisition.lease().failureReason()); - assertFalse(state.engine.isLoaded()); - } - } - - @Test - void trackingFenceTimeoutsFailPendingHandoffExactlyOnce() throws Exception { - TimeoutLeelaz initialEngine = reusableTimeoutKatago(); - initialEngine.initialStopTimeoutMillis = 25L; - try (TestState state = TestState.open(initialEngine)) { - RecordingHandoffTarget target = RecordingHandoffTarget.retained(); - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - - waitUntil(() -> target.failures.get() == 1); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(0, target.activations.get()); - assertEquals(1, target.failures.get()); - } - - TimeoutLeelaz finalEngine = reusableTimeoutKatago(); - finalEngine.releaseStopTimeoutMillis = 25L; - try (TestState state = TestState.open(finalEngine)) { - RecordingHandoffTarget target = RecordingHandoffTarget.foreground(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - Leelaz.TrackingHandoffClaim claim = state.engine.claimTrackingHandoff(target); - - waitUntil(() -> target.failures.get() == 1); - - assertEquals(Leelaz.TrackingHandoffState.FAILED, claim.state()); - assertEquals(0, target.activations.get()); - assertEquals(1, target.failures.get()); - } - } - - @Test - void partialActiveCommandWriteInvalidatesTransportAndClosesLease() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - installOutput(state.engine, Leelaz.createCommandOutputStream(new PartialCommandOutput(4))); - - assertFalse(acquisition.lease().send("kata-analyze B 10")); - - assertEquals(1, closed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.ACTIVE_COMMAND_SEND_FAILED), - acquisition.lease().failureReason()); - assertFalse(state.engine.isLoaded()); - assertEquals(null, currentOutput(state.engine)); - } - } - - @Test - void trackingAdmissionRejectsRemoteAndIncompleteCommandDiscovery() throws Exception { - Leelaz remote = reusableLocalKatago(); - remote.useRemoteCompute = true; - try (TestState state = TestState.open(remote)) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY, acquisition.availability()); - assertEquals(null, acquisition.lease()); - assertEquals("", state.output.toString(StandardCharsets.UTF_8)); - } - - Leelaz missingAnalyze = reusableLocalKatago(); - missingAnalyze.commandLists.remove("kata-analyze"); - try (TestState state = TestState.open(missingAnalyze)) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.MISSING_CAPABILITY, acquisition.availability()); - assertEquals(null, acquisition.lease()); - } - } - - @Test - void initialStopSendFailureReturnsStableAcquisitionFailure() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - installOutput( - state.engine, - Leelaz.createCommandOutputStream( - new OutputStream() { - @Override - public void write(int value) throws IOException { - throw new IOException("simulated initial stop failure"); - } - })); - - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - state.engine.sendCommand("version"); - - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY, acquisition.availability()); - assertEquals(null, acquisition.lease()); - assertEquals(null, acquisition.receipt()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.INITIAL_STOP_SEND_FAILED), - acquisition.failureReason()); - assertEquals(1, closed.get()); - assertFalse(state.engine.isLoaded()); - assertEquals(null, currentOutput(state.engine)); - } - } - - @Test - void initialBoundaryCannotPublishReadyBeforeWriteResult() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - BlockingFailingFlushOutput blockedOutput = new BlockingFailingFlushOutput(); - installOutput(state.engine, Leelaz.createCommandOutputStream(blockedOutput)); - AtomicInteger ready = new AtomicInteger(); - AtomicInteger closed = new AtomicInteger(); - AtomicReference acquisition = new AtomicReference<>(); - Thread acquireThread = - new Thread( - () -> - acquisition.set( - state.engine.acquireTrackingStreamLease( - line -> {}, - lease -> ready.incrementAndGet(), - lease -> closed.incrementAndGet())), - "tracking-initial-write-result"); - acquireThread.setDaemon(true); - - acquireThread.start(); - assertTrue(blockedOutput.flushStarted.await(1, TimeUnit.SECONDS)); - int readyBeforeWriteResult; - int closedBeforeWriteResult; - try { - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - readyBeforeWriteResult = ready.get(); - closedBeforeWriteResult = closed.get(); - } finally { - blockedOutput.failFlush.countDown(); - } - acquireThread.join(1000L); - - assertFalse(acquireThread.isAlive()); - assertEquals(0, readyBeforeWriteResult); - assertEquals(0, closedBeforeWriteResult); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY, acquisition.get().availability()); - assertEquals(null, acquisition.get().lease()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.INITIAL_STOP_SEND_FAILED), - acquisition.get().failureReason()); - assertEquals(0, ready.get()); - assertEquals(1, closed.get()); - assertFalse(state.engine.isLoaded()); - } - } - - @Test - void releaseBeforeActiveSendClaimDoesNotFailTheTransport() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(acquisition.lease().release()); - assertFalse(acquisition.lease().send("kata-analyze B 10")); - assertEquals(java.util.Optional.empty(), acquisition.lease().failureReason()); - assertTrue(state.engine.isLoaded()); - assertEquals( - "800000000 stop\n800000001 stop\n", state.output.toString(StandardCharsets.UTF_8)); - assertTrue(dispatch(state.engine, "=800000001")); - assertTrue(dispatch(state.engine, "")); - assertEquals(1, closed.get()); - } - } - - @Test - void activeSendFailureAfterClaimStillFailsClosedWhenReleaseRaces() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - BlockingOneShotPartialOutput partialOutput = new BlockingOneShotPartialOutput(4); - installOutput(state.engine, Leelaz.createCommandOutputStream(partialOutput)); - AtomicReference sendResult = new AtomicReference<>(); - AtomicReference releaseResult = new AtomicReference<>(); - Thread sendThread = - new Thread( - () -> sendResult.set(acquisition.lease().send("kata-analyze B 10")), - "tracking-send-claimed-failure"); - Thread releaseThread = - new Thread( - () -> releaseResult.set(acquisition.lease().release()), - "tracking-release-after-send-claim"); - sendThread.setDaemon(true); - releaseThread.setDaemon(true); - - sendThread.start(); - assertTrue(partialOutput.writeBlocked.await(1, TimeUnit.SECONDS)); - releaseThread.start(); - waitUntil( - () -> releaseResult.get() != null || releaseThread.getState() == Thread.State.BLOCKED); - try { - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - assertEquals(0, closed.get()); - assertTrue(acquisition.lease().isOwned()); - } finally { - partialOutput.failClaimedWrite.countDown(); - } - sendThread.join(1000L); - releaseThread.join(1000L); - - assertFalse(sendThread.isAlive()); - assertFalse(releaseThread.isAlive()); - assertFalse(sendResult.get()); - assertTrue(releaseResult.get()); - assertEquals(1, closed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.ACTIVE_COMMAND_SEND_FAILED), - acquisition.lease().failureReason()); - assertFalse(state.engine.isLoaded()); - assertEquals(null, currentOutput(state.engine)); - } - } - - @Test - void trackingLeaseAllowsOnlyOneActiveCommand() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - - assertTrue(acquisition.lease().send("kata-analyze B 10")); - assertFalse(acquisition.lease().send("kata-analyze W 10")); - - assertEquals( - "800000000 stop\n800000001 kata-analyze B 10\n", - state.output.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void finalFenceDoesNotCloseBeforeAnalyzeTerminator() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - state.engine.sendCommand("version"); - - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - - assertEquals(0, closed.get()); - assertTrue(acquisition.lease().isOwned()); - assertFalse(state.output.toString(StandardCharsets.UTF_8).contains("version\n")); - - assertTrue(dispatch(state.engine, "=800000002")); - assertTrue(dispatch(state.engine, "")); - assertEquals(1, closed.get()); - assertTrue(state.output.toString(StandardCharsets.UTF_8).endsWith("version\n")); - } - } - - @Test - void finalBoundaryCannotCloseBeforeWriteResult() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - AtomicInteger closed = new AtomicInteger(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease( - line -> {}, lease -> {}, lease -> closed.incrementAndGet()); - processCommandResponse(state.engine, "=800000000"); - assertTrue(dispatch(state.engine, "")); - assertTrue(acquisition.lease().send("kata-analyze B 10")); - BlockingFailingFlushOutput blockedOutput = new BlockingFailingFlushOutput(); - installOutput(state.engine, Leelaz.createCommandOutputStream(blockedOutput)); - AtomicReference commandFinished = new AtomicReference<>(false); - Thread releaseThread = - new Thread( - () -> { - state.engine.sendCommand("version"); - commandFinished.set(true); - }, - "tracking-final-write-result"); - releaseThread.setDaemon(true); - - releaseThread.start(); - assertTrue(blockedOutput.flushStarted.await(1, TimeUnit.SECONDS)); - assertTrue(dispatch(state.engine, "")); - assertTrue(dispatch(state.engine, "=800000002")); - AtomicReference frameFailure = new AtomicReference<>(); - Thread frameThread = - new Thread( - () -> { - try { - dispatch(state.engine, ""); - } catch (Throwable failure) { - frameFailure.set(failure); - } - }, - "tracking-early-final-boundary"); - frameThread.setDaemon(true); - frameThread.start(); - waitUntil(() -> !acquisition.lease().isOwned() || !frameThread.isAlive()); - boolean ownedBeforeWriteResult = acquisition.lease().isOwned(); - int closedBeforeWriteResult = closed.get(); - blockedOutput.failFlush.countDown(); - releaseThread.join(1000L); - frameThread.join(1000L); - - assertFalse(releaseThread.isAlive()); - assertFalse(frameThread.isAlive()); - assertTrue(ownedBeforeWriteResult); - assertEquals(0, closedBeforeWriteResult); - assertEquals(null, frameFailure.get()); - assertTrue(commandFinished.get()); - assertEquals(1, closed.get()); - assertEquals( - java.util.Optional.of(Leelaz.TrackingStreamLeaseFailure.FINAL_STOP_SEND_FAILED), - acquisition.lease().failureReason()); - assertFalse(state.engine.isLoaded()); - assertFalse(blockedOutput.output.toString(StandardCharsets.UTF_8).contains("version\n")); - } - } - - @Test - void trackingAdmissionRejectsForegroundBusinessOwners() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - EngineManager previousManager = Lizzie.engineManager; - Leelaz dummy = new Leelaz(""); - EngineManager manager = new EngineManager(List.of(state.engine, dummy)); - try { - Lizzie.engineManager = manager; - EngineManager.resetEngineGameTransactionStateForTest(); - EngineManager.beginEngineGameTransaction( - manager, EngineGamePlans.harness(0, 1, false), null, true); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.ENGINE_GAME, - state - .engine - .acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}) - .availability()); - } finally { - EngineManager.resetEngineGameTransactionStateForTest(); - Lizzie.engineManager = previousManager; - } - - state.engine.isThinking = true; - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.GENMOVE, - state - .engine - .acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}) - .availability()); - state.engine.isThinking = false; - - Lizzie.frame.isPlayingAgainstLeelaz = true; - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.PLAY_MODE, - state - .engine - .acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}) - .availability()); - Lizzie.frame.isPlayingAgainstLeelaz = false; - - Lizzie.frame.isContributing = true; - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE, - state - .engine - .acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}) - .availability()); - } - } - - @Test - void trackingAdmissionDoesNotWaitForOrdinaryWriteInFlight() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - BlockingOutput blockingOutput = new BlockingOutput(); - installOutput(state.engine, Leelaz.createCommandOutputStream(blockingOutput)); - Thread commandThread = new Thread(() -> state.engine.sendCommand("name")); - commandThread.setDaemon(true); - commandThread.start(); - assertTrue(blockingOutput.writeStarted.await(1, TimeUnit.SECONDS)); - - Leelaz.TrackingStreamLeaseAcquisition acquisition = - state.engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.ENGINE_NOT_READY, acquisition.availability()); - assertEquals(null, acquisition.lease()); - blockingOutput.continueWrite.countDown(); - commandThread.join(1000L); - assertFalse(commandThread.isAlive()); - assertFalse(blockingOutput.output.toString(StandardCharsets.UTF_8).contains("stop\n")); - } - } - - @Test - void readerRebindWaitsForClaimedOrdinaryWriteBeforeCutover() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - ensureReaderStreamBinding(state.engine); - BlockingOutput oldOutput = new BlockingOutput(); - installOutput(state.engine, Leelaz.createCommandOutputStream(oldOutput)); - Thread commandThread = new Thread(() -> state.engine.sendCommand("version")); - commandThread.setDaemon(true); - commandThread.start(); - assertTrue(oldOutput.writeStarted.await(1, TimeUnit.SECONDS)); - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - AtomicReference rebindFailure = new AtomicReference<>(); - Thread rebindThread = - new Thread( - () -> { - try { - initializeStreams(state.engine, reboundOutput); - } catch (Throwable failure) { - rebindFailure.set(failure); - } - }, - "rebind-during-claimed-ordinary-write"); - rebindThread.setDaemon(true); - rebindThread.start(); - waitUntil( - () -> - !rebindThread.isAlive() - || rebindThread.getState() == Thread.State.WAITING); - boolean rebindWaitedForWrite = rebindThread.isAlive(); - oldOutput.continueWrite.countDown(); - commandThread.join(1000L); - rebindThread.join(1000L); - - assertTrue(rebindWaitedForWrite); - assertFalse(commandThread.isAlive()); - assertFalse(rebindThread.isAlive()); - assertEquals(null, rebindFailure.get()); - assertTrue(oldOutput.output.toString(StandardCharsets.UTF_8).contains("version\n")); - assertEquals("", reboundOutput.toString(StandardCharsets.UTF_8)); - } - } - - @Test - void readerRebindRetiresSentTrackedLoadSgfHandlerAtCutover() throws Exception { - try (TestState state = TestState.open(reusableLocalKatago())) { - ensureReaderStreamBinding(state.engine); - ByteArrayOutputStream oldOutput = installOutput(state.engine); - AtomicInteger stateResetCallbacks = new AtomicInteger(); - sendTrackedLoadSgf(state.engine, stateResetCallbacks); - assertTrue(oldOutput.toString(StandardCharsets.UTF_8).contains("loadsgf ")); - assertEquals(1, pendingResponseHandlerCount(state.engine)); - - ByteArrayOutputStream reboundOutput = new ByteArrayOutputStream(); - initializeStreams(state.engine, reboundOutput); - - assertEquals(1, stateResetCallbacks.get()); - assertEquals(0, pendingResponseHandlerCount(state.engine)); - processCommandResponse(state.engine, "=1"); - assertEquals(1, stateResetCallbacks.get()); - } - } - - private Leelaz reusableLocalKatago() throws Exception { - Leelaz engine = new Leelaz(""); - configureLocalKatago(engine); - createdEngines.add(engine); - return engine; - } - - private TimeoutLeelaz reusableTimeoutKatago() throws Exception { - TimeoutLeelaz engine = new TimeoutLeelaz(); - configureLocalKatago(engine); - createdEngines.add(engine); - return engine; - } - - private static void configureLocalKatago(Leelaz engine) throws Exception { - engine.isLoaded = true; - engine.started = true; - engine.isKatago = true; - engine.commandLists.addAll(List.of("stop", "kata-analyze")); - Field capabilityDiscovery = Leelaz.class.getDeclaredField("endGetCommandList"); - capabilityDiscovery.setAccessible(true); - capabilityDiscovery.set(engine, true); - } - - private static ByteArrayOutputStream installOutput(Leelaz engine) throws Exception { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - engine.installCommandOutputForTest(new BufferedOutputStream(output)); - return output; - } - - private static void installOutput(Leelaz engine, BufferedOutputStream output) throws Exception { - engine.installCommandOutputForTest(output); - } - - private static BufferedOutputStream currentOutput(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("outputStream"); - field.setAccessible(true); - return (BufferedOutputStream) field.get(engine); - } - - private static boolean readerStreamRebindInProgress(Leelaz engine) { - try { - Field field = Leelaz.class.getDeclaredField("readerStreamRebindInProgress"); - field.setAccessible(true); - return field.getBoolean(engine); - } catch (ReflectiveOperationException failure) { - throw new AssertionError(failure); - } - } - - private static void notifyEngineArbitrationWaiters(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("engineArbitrationLock"); - field.setAccessible(true); - Object lock = field.get(engine); - synchronized (lock) { - lock.notifyAll(); - } - } - - private static Object commandQueue(Leelaz engine) { - try { - Method method = Leelaz.class.getDeclaredMethod("commandQueue"); - method.setAccessible(true); - return method.invoke(engine); - } catch (ReflectiveOperationException failure) { - throw new AssertionError(failure); - } - } - - - private static int commandQueueSize(Leelaz engine) { - try { - Method method = Leelaz.class.getDeclaredMethod("commandQueue"); - method.setAccessible(true); - return ((java.util.Collection) method.invoke(engine)).size(); - } catch (ReflectiveOperationException failure) { - throw new AssertionError(failure); - } - } - - private static int pendingResponseHandlerCount(Leelaz engine) { - try { - Field field = Leelaz.class.getDeclaredField("pendingResponseHandlers"); - field.setAccessible(true); - Object handlers = field.get(engine); - return handlers == null ? 0 : ((java.util.Collection) handlers).size(); - } catch (ReflectiveOperationException failure) { - throw new AssertionError(failure); - } - } - - private static void closeExclusiveSessionForTest(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("exclusiveGtpSession"); - field.setAccessible(true); - Object session = field.get(engine); - if (session == null) { - return; - } - Method cancelInitial = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpInitialStopTimeout", session.getClass()); - cancelInitial.setAccessible(true); - cancelInitial.invoke(engine, session); - Method cancelRelease = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpReleaseStopTimeout", session.getClass()); - cancelRelease.setAccessible(true); - cancelRelease.invoke(engine, session); - Method close = Leelaz.class.getDeclaredMethod("closeExclusiveGtpSession", session.getClass()); - close.setAccessible(true); - close.invoke(engine, session); - } - - private static void ensureReaderStreamBinding(Leelaz engine) { - try { - Method method = Leelaz.class.getDeclaredMethod("currentReaderStreamBinding"); - method.setAccessible(true); - method.invoke(engine); - } catch (ReflectiveOperationException failure) { - throw new AssertionError(failure); - } - } - - private static void initializeStreams(Leelaz engine, ByteArrayOutputStream output) - throws Exception { - initializeStreams(engine, "", output); - } - - private static void initializeStreams(Leelaz engine, String stdout, ByteArrayOutputStream output) - throws Exception { - Method method = - Leelaz.class.getDeclaredMethod( - "initializeStreams", - java.io.InputStream.class, - java.io.OutputStream.class, - java.io.InputStream.class); - method.setAccessible(true); - method.invoke( - engine, - new ByteArrayInputStream(stdout.getBytes(StandardCharsets.UTF_8)), - output, - new ByteArrayInputStream(new byte[0])); - } - - private static void enqueueThrowingTrackedLoadSgf( - Leelaz engine, CountDownLatch callbackStarted, CountDownLatch failCallback) throws Exception { - enqueueBlockingTrackedLoadSgf( - engine, - callbackStarted, - failCallback, - new IllegalStateException("simulated rebind callback failure")); - } - - private static void sendTrackedLoadSgf(Leelaz engine, AtomicInteger stateResetCallbacks) - throws Exception { - Class failureHandlerType = - Class.forName("featurecat.lizzie.analysis.Leelaz$CommandSendFailureHandler"); - Object failureHandler = - java.lang.reflect.Proxy.newProxyInstance( - failureHandlerType.getClassLoader(), - new Class[] {failureHandlerType}, - (proxy, method, arguments) -> { - if (method.getName().equals("onStateResetAfterOutputWrite")) { - stateResetCallbacks.incrementAndGet(); - } - return null; - }); - Method method = - Leelaz.class.getDeclaredMethod( - "sendCommand", - String.class, - Runnable.class, - failureHandlerType, - boolean.class, - boolean.class); - method.setAccessible(true); - method.invoke( - engine, "loadsgf /tmp/tracking-rebind-sent.sgf", null, failureHandler, true, false); - } - - private static void enqueueBlockingTrackedLoadSgf( - Leelaz engine, - CountDownLatch callbackStarted, - CountDownLatch continueCallback, - RuntimeException callbackFailure) - throws Exception { - enqueueBlockingTrackedLoadSgf( - engine, callbackStarted, continueCallback, callbackFailure, null); - } - - private static void enqueueBlockingTrackedLoadSgf( - Leelaz engine, - CountDownLatch callbackStarted, - CountDownLatch continueCallback, - RuntimeException callbackFailure, - AtomicInteger stateResetCallbacks) - throws Exception { - Class failureHandlerType = - Class.forName("featurecat.lizzie.analysis.Leelaz$CommandSendFailureHandler"); - Object failureHandler = - java.lang.reflect.Proxy.newProxyInstance( - failureHandlerType.getClassLoader(), - new Class[] {failureHandlerType}, - (proxy, method, arguments) -> { - if (stateResetCallbacks != null - && method.getName().equals("onStateResetAfterOutputWrite")) { - stateResetCallbacks.incrementAndGet(); - } - callbackStarted.countDown(); - if (!continueCallback.await(1, TimeUnit.SECONDS)) { - throw new IllegalStateException("timed out waiting to continue reset callback"); - } - if (callbackFailure != null) { - throw callbackFailure; - } - return null; - }); - Method method = - Leelaz.class.getDeclaredMethod( - "sendCommand", - String.class, - Runnable.class, - failureHandlerType, - boolean.class, - boolean.class); - method.setAccessible(true); - method.invoke(engine, "loadsgf /tmp/tracking-rebind.sgf", null, failureHandler, true, false); - } - - private static void installInput(Leelaz engine, String input) throws Exception { - Field field = Leelaz.class.getDeclaredField("inputStream"); - field.setAccessible(true); - field.set(engine, new BufferedReader(new StringReader(input))); - } - - private static void invokeRead(Leelaz engine) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("read"); - method.setAccessible(true); - method.invoke(engine); - } - - private static boolean dispatch(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } - - private static void processCommandResponse(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); - } - - private static void waitUntil(Check condition) throws Exception { - long deadline = System.currentTimeMillis() + 3000L; - while (!condition.get() && System.currentTimeMillis() < deadline) { - Thread.sleep(10L); - } - assertTrue(condition.get(), "timed out waiting for tracking lease state"); - } - - @SuppressWarnings("unchecked") - private static T allocate(Class type) throws Exception { - Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); - unsafeField.setAccessible(true); - return (T) ((Unsafe) unsafeField.get(null)).allocateInstance(type); - } - - private static final class SilentGtpConsole extends GtpConsolePane { - private SilentGtpConsole() { - super(null); - } - - @Override - public void addCommand(String command, int commandNumber, String engineName) {} - } - - private static final class RecordingMenu extends Menu { - private List> transitions; - - private RecordingMenu() {} - - @Override - public void toggleEngineMenuStatus(boolean isPondering, boolean isThinking) { - transitions.add(List.of(isPondering, isThinking)); - } - } - - private static final class RecordingBoard extends Board { - private int resendCount; - - private RecordingBoard() { - super(); - } - - @Override - public void resendMoveToEngine(Leelaz engine, boolean loadEngine) { - resendCount++; - } - } - - @FunctionalInterface - private interface Check { - boolean get() throws Exception; - } - - private static final class PartialCommandOutput extends OutputStream { - private int remainingSuccessfulBytes; - - private PartialCommandOutput(int remainingSuccessfulBytes) { - this.remainingSuccessfulBytes = remainingSuccessfulBytes; - } - - @Override - public void write(int value) throws IOException { - if (remainingSuccessfulBytes-- <= 0) { - throw new IOException("simulated partial tracking write"); - } - } - } - - private static final class BlockingOutput extends OutputStream { - private final CountDownLatch writeStarted = new CountDownLatch(1); - private final CountDownLatch continueWrite = new CountDownLatch(1); - private final ByteArrayOutputStream output = new ByteArrayOutputStream(); - private boolean blocked; - - @Override - public synchronized void write(int value) throws IOException { - if (!blocked) { - blocked = true; - writeStarted.countDown(); - try { - if (!continueWrite.await(1, TimeUnit.SECONDS)) { - throw new IOException("timed out waiting to continue ordinary write"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IOException(interrupted); - } - } - output.write(value); - } - } - - private static final class BlockingOneShotPartialOutput extends OutputStream { - private final CountDownLatch writeBlocked = new CountDownLatch(1); - private final CountDownLatch failClaimedWrite = new CountDownLatch(1); - private int remainingSuccessfulBytes; - private boolean failed; - - private BlockingOneShotPartialOutput(int remainingSuccessfulBytes) { - this.remainingSuccessfulBytes = remainingSuccessfulBytes; - } - - @Override - public synchronized void write(int value) throws IOException { - if (!failed && remainingSuccessfulBytes-- <= 0) { - writeBlocked.countDown(); - try { - if (!failClaimedWrite.await(1, TimeUnit.SECONDS)) { - throw new IOException("timed out waiting to fail claimed tracking write"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IOException(interrupted); - } - failed = true; - throw new IOException("simulated claimed partial tracking write"); - } - } - } - - private static final class BlockingFailingFlushOutput extends OutputStream { - private final CountDownLatch flushStarted = new CountDownLatch(1); - private final CountDownLatch failFlush = new CountDownLatch(1); - private final ByteArrayOutputStream output = new ByteArrayOutputStream(); - - @Override - public synchronized void write(int value) { - output.write(value); - } - - @Override - public void flush() throws IOException { - flushStarted.countDown(); - try { - if (!failFlush.await(1, TimeUnit.SECONDS)) { - throw new IOException("timed out waiting to fail tracking flush"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IOException(interrupted); - } - throw new IOException("simulated tracking flush failure"); - } - } - - private static final class TimeoutLeelaz extends Leelaz { - private long initialStopTimeoutMillis = 8000L; - private long releaseStopTimeoutMillis = 8000L; - private boolean blockTimeout; - private final CountDownLatch timeoutStarted = new CountDownLatch(1); - private final CountDownLatch continueTimeout = new CountDownLatch(1); - private final CountDownLatch timeoutFinished = new CountDownLatch(1); - private final AtomicReference timeoutFailure = new AtomicReference<>(); - - private TimeoutLeelaz() throws Exception { - super(""); - } - - @Override - protected long foregroundInitialStopTimeoutMillis() { - return initialStopTimeoutMillis; - } - - @Override - protected long foregroundReleaseStopTimeoutMillis() { - return releaseStopTimeoutMillis; - } - - @Override - void executeForegroundInitialStopTimeout(Runnable timeoutAction) { - executeTimeout(timeoutAction); - } - - @Override - void executeForegroundReleaseStopTimeout(Runnable timeoutAction) { - executeTimeout(timeoutAction); - } - - private void executeTimeout(Runnable timeoutAction) { - if (!blockTimeout) { - timeoutAction.run(); - return; - } - timeoutStarted.countDown(); - try { - if (!continueTimeout.await(1, TimeUnit.SECONDS)) { - throw new IllegalStateException("timed out waiting to continue tracking timeout"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(interrupted); - } - try { - timeoutAction.run(); - } catch (Throwable failure) { - timeoutFailure.compareAndSet(null, failure); - } finally { - timeoutFinished.countDown(); - } - } - } - - private static final class FeedbackRecordingLeelaz extends Leelaz { - private final AtomicInteger feedbackCount = new AtomicInteger(); - - private FeedbackRecordingLeelaz() throws Exception { - super(""); - } - - @Override - void showExclusiveGtpConflictMessage() { - feedbackCount.incrementAndGet(); - } - } - - private static final class PonderTrackingFrame extends LizzieFrame { - private int redrawCount; - private int refreshCount; - - @Override - public void clearTryPlay() {} - - @Override - public void clearSelectImage() {} - - @Override - public void onMainEnginePonder() {} - - @Override - public void resetTitle() {} - - @Override - public void clearKataEstimate() {} - - @Override - public void redrawBoardrendererBackground() { - redrawCount++; - } - - @Override - public void refresh() { - refreshCount++; - } - } - - private static final class HeldLifecycleReservation implements AutoCloseable { - private final CountDownLatch acquired = new CountDownLatch(1); - private final CountDownLatch release = new CountDownLatch(1); - private final AtomicReference failure = new AtomicReference<>(); - private final Thread owner; - - private HeldLifecycleReservation(Leelaz engine) { - owner = - new Thread( - () -> { - Leelaz.ExclusiveGtpLifecycleReservation reservation = null; - try { - reservation = engine.beginExclusiveGtpLifecycleReservation(); - if (reservation == null) { - throw new AssertionError("failed to acquire mirror lifecycle reservation"); - } - acquired.countDown(); - release.await(); - } catch (Throwable reservationFailure) { - failure.compareAndSet(null, reservationFailure); - } finally { - acquired.countDown(); - if (reservation != null) { - try { - reservation.close(); - } catch (Throwable closeFailure) { - failure.compareAndSet(null, closeFailure); - } - } - } - }, - "held-mirror-lifecycle-reservation"); - owner.setDaemon(true); - } - - private static HeldLifecycleReservation open(Leelaz engine) throws Exception { - HeldLifecycleReservation held = new HeldLifecycleReservation(engine); - held.owner.start(); - try { - if (!held.acquired.await(2, TimeUnit.SECONDS)) { - throw new AssertionError("timed out acquiring mirror lifecycle reservation"); - } - held.rethrowFailure(); - return held; - } catch (Throwable openFailure) { - try { - held.close(); - } catch (Throwable cleanupFailure) { - if (cleanupFailure != openFailure) { - openFailure.addSuppressed(cleanupFailure); - } - } - if (openFailure instanceof Exception) { - throw (Exception) openFailure; - } - if (openFailure instanceof Error) { - throw (Error) openFailure; - } - throw new AssertionError(openFailure); - } - } - - @Override - public void close() throws Exception { - release.countDown(); - owner.join(TimeUnit.SECONDS.toMillis(2)); - if (owner.isAlive()) { - throw new AssertionError("mirror lifecycle reservation owner did not stop"); - } - rethrowFailure(); - } - - private void rethrowFailure() throws Exception { - Throwable reservationFailure = failure.get(); - if (reservationFailure == null) { - return; - } - if (reservationFailure instanceof Exception) { - throw (Exception) reservationFailure; - } - if (reservationFailure instanceof Error) { - throw (Error) reservationFailure; - } - throw new AssertionError(reservationFailure); - } - } - - private static class RecordingHandoffTarget implements Leelaz.TrackingHandoffTarget { - private final Leelaz.TrackingHandoffKind kind; - protected final AtomicInteger activations = new AtomicInteger(); - protected final AtomicInteger failures = new AtomicInteger(); - - private RecordingHandoffTarget(Leelaz.TrackingHandoffKind kind) { - this.kind = kind; - } - - private static RecordingHandoffTarget foreground() { - return new RecordingHandoffTarget(Leelaz.TrackingHandoffKind.FOREGROUND_ANALYSIS); - } - - private static RecordingHandoffTarget retained() { - return new RecordingHandoffTarget(Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE); - } - - @Override - public Leelaz.TrackingHandoffKind kind() { - return kind; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - activations.incrementAndGet(); - if (kind == Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE) { - assertTrue(activation.completeRetainedEngineMode()); - } else { - assertTrue(activation.activateForegroundAnalysis(line -> {}, () -> {})); - } - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failures.incrementAndGet(); - } - } - - private static final class RecordingDispositionObserver - implements Leelaz.TrackingReleaseDispositionObserver { - private final List reasons = new ArrayList<>(); - private final List dispositions = new ArrayList<>(); - - @Override - public void onDispositionChanged(Leelaz.TrackingReleaseDisposition disposition) { - dispositions.add(disposition); - } - - @Override - public void onReleaseClaimed(Leelaz.TrackingReleaseReason reason) { - reasons.add(reason); - } - } - - private static final class BlockingHandoffTarget extends RecordingHandoffTarget { - private final CountDownLatch activationStarted = new CountDownLatch(1); - private final CountDownLatch allowCompletion = new CountDownLatch(1); - private final CountDownLatch failureStarted = new CountDownLatch(1); - private final CountDownLatch allowFailureCompletion = new CountDownLatch(1); - private final Runnable beforeCompletion; - private final boolean blockFailure; - - private BlockingHandoffTarget() { - this(() -> {}, false); - } - - private BlockingHandoffTarget(Runnable beforeCompletion, boolean blockFailure) { - super(Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE); - this.beforeCompletion = beforeCompletion; - this.blockFailure = blockFailure; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - activationStarted.countDown(); - try { - assertTrue(allowCompletion.await(1, TimeUnit.SECONDS)); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new AssertionError(interrupted); - } - beforeCompletion.run(); - super.activate(activation); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - failureStarted.countDown(); - if (blockFailure) { - try { - assertTrue(allowFailureCompletion.await(1, TimeUnit.SECONDS)); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new AssertionError(interrupted); - } - } - super.fail(failure); - } - } - - private enum TrackingTimeoutPhase { - INITIAL { - @Override - void useShortTimeout(TimeoutLeelaz engine) { - engine.initialStopTimeoutMillis = 25L; - } - - @Override - void startTimeout(Leelaz engine, Leelaz.TrackingStreamLease lease) {} - - @Override - Leelaz.TrackingStreamLeaseFailure failure() { - return Leelaz.TrackingStreamLeaseFailure.INITIAL_STOP_TIMEOUT; - } - }, - FINAL { - @Override - void useShortTimeout(TimeoutLeelaz engine) { - engine.releaseStopTimeoutMillis = 25L; - } - - @Override - void startTimeout(Leelaz engine, Leelaz.TrackingStreamLease lease) throws Exception { - processCommandResponse(engine, "=800000000"); - assertTrue(dispatch(engine, "")); - assertTrue(lease.release()); - } - - @Override - Leelaz.TrackingStreamLeaseFailure failure() { - return Leelaz.TrackingStreamLeaseFailure.FINAL_STOP_TIMEOUT; - } - }; - - abstract void useShortTimeout(TimeoutLeelaz engine); - - abstract void startTimeout(Leelaz engine, Leelaz.TrackingStreamLease lease) throws Exception; - - void startTimeoutAfterPendingLoadSgf(Leelaz engine, Leelaz.TrackingStreamLease lease) - throws Exception { - startTimeout(engine, lease); - } - - abstract Leelaz.TrackingStreamLeaseFailure failure(); - } - - private enum SuccessfulTrackingClosePhase { - ACQUIRING { - @Override - void prepareCloseBoundary(Leelaz engine, Leelaz.TrackingStreamLease lease) - throws Exception { - assertTrue(lease.release()); - processCommandResponse(engine, "=800000000"); - } - }, - FINAL { - @Override - void prepareCloseBoundary(Leelaz engine, Leelaz.TrackingStreamLease lease) - throws Exception { - processCommandResponse(engine, "=800000000"); - assertTrue(dispatch(engine, "")); - assertTrue(lease.release()); - assertTrue(dispatch(engine, "=800000001")); - } - }; - - abstract void prepareCloseBoundary(Leelaz engine, Leelaz.TrackingStreamLease lease) - throws Exception; - } - - private static final class TestState implements AutoCloseable { - private final Leelaz previousEngine; - private final LizzieFrame previousFrame; - private final Config previousConfig; - private final GtpConsolePane previousGtpConsole; - private final Menu previousMenu; - private final Leelaz engine; - private final ByteArrayOutputStream output; - private final RecordingMenu menu; - - private TestState(Leelaz engine) throws Exception { - previousEngine = Lizzie.leelaz; - previousFrame = Lizzie.frame; - previousConfig = Lizzie.config; - previousGtpConsole = Lizzie.gtpConsole; - previousMenu = LizzieFrame.menu; - this.engine = engine; - output = installOutput(engine); - Lizzie.leelaz = engine; - Lizzie.frame = allocate(LizzieFrame.class); - Lizzie.config = allocate(Config.class); - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - menu = allocate(RecordingMenu.class); - menu.transitions = new ArrayList<>(); - LizzieFrame.menu = menu; - } - - private static TestState open(Leelaz engine) throws Exception { - return new TestState(engine); - } - - @Override - public void close() throws Exception { - closeExclusiveSessionForTest(engine); - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - LizzieFrame.menu = previousMenu; - } - } -} diff --git a/src/test/java/featurecat/lizzie/analysis/MoveDataParseTest.java b/src/test/java/featurecat/lizzie/analysis/MoveDataParseTest.java index b86cfefab..c3c9dae56 100644 --- a/src/test/java/featurecat/lizzie/analysis/MoveDataParseTest.java +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataParseTest.java @@ -167,6 +167,76 @@ void branchLimitKeepsVariationAndPvVisitsCompact() { } } + @Test + void missingEdgePreservesLegacySymmetricCandidatePercentages() { + KataGoAnalysisPayload payload = KataGoAnalysisPayload.parse( + "info move D4 visits 10 pv D4 info move F6 visits 10 isSymmetryOf D4 pv F6"); + assertEquals(10, payload.totalVisits()); + assertEquals(0.5, + payload.moves.get(0).allocationRatio(MoveData.getAllocationVisits(payload.moves))); + } + + @Test + void upstreamRootCountIsIndependentOfCandidateResearch() throws Exception { + String line; + try (var input = getClass().getResourceAsStream("/analysis/katago-root-focus-info.txt")) { + line = new String(input.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + KataGoAnalysisPayload payload = KataGoAnalysisPayload.parse(line); + assertEquals(100, payload.rootVisits); + assertEquals(100, payload.totalVisits()); + assertEquals(848, MoveData.getPlayouts(payload.moves)); + MoveData c3 = payload.moves.stream().filter(move -> move.coordinate.equals("C3")).findFirst().orElseThrow(); + assertEquals(368, c3.playouts); + assertEquals(0, c3.edgeVisits); + assertEquals(10, c3.order); + assertFalse(c3.variation.contains("rootInfo")); + assertEquals(0.0, c3.allocationRatio(MoveData.getAllocationVisits(payload.moves))); + assertEquals(0.0, c3.allocationRatio(0)); + KataGoAnalysisPayload legacy = KataGoAnalysisPayload.parse("info move D4 visits 40 pv D4"); + assertEquals(-1, legacy.rootVisits); + assertEquals(40, legacy.totalVisits()); + assertEquals(1.0, legacy.moves.get(0).allocationRatio(40)); + } + + @Test + void frameSeparatesOwnershipAndPvArrays() { + KataGoAnalysisPayload payload = KataGoAnalysisPayload.parse( + "info move C3 visits 8 pv C3 D4 pvVisits 5 3 pvEdgeVisits 2 1" + + " movesOwnership 0.5 -0.5 rootInfo visits 4 winrate 0.6" + + " ownership 0.25 -0.25 ownershipStdev 0.1 0.2"); + assertEquals(4, payload.totalVisits()); + assertEquals(List.of(0.25, -0.25), payload.ownership); + assertEquals(List.of(0.5, -0.5), payload.moves.get(0).movesEstimateArray); + assertEquals(List.of("2", "1"), payload.moves.get(0).pvEdgeVisits); + } + + @Test + void preservesZeroEdgeAndSeparatePvStatistics() { + MoveData move = + MoveData.fromInfoKatago( + "move C3 visits 368 edgeVisits 0 order 10 pv C3 D4" + + " pvVisits 300 20 pvEdgeVisits 0 10 movesOwnership 0.5 -0.5" + + " rootInfo visits 100 ownership 0.25 -0.25"); + assertEquals(368, move.playouts); + assertEquals(0, move.edgeVisits); + assertEquals(10, move.order); + assertEquals(List.of("C3", "D4"), move.variation); + assertEquals(List.of("300", "20"), move.pvVisits); + assertEquals(List.of("0", "10"), move.pvEdgeVisits); + assertEquals(List.of(0.5, -0.5), move.movesEstimateArray); + assertEquals(-1, MoveData.fromInfoKatago("move D4 visits 1 pv D4").edgeVisits); + } + + @Test + void rootAndOwnershipNeverBecomeCandidateVariation() { + MoveData move = + MoveData.fromInfoKatago( + "move C3 visits 368 order 10 pv C3 D4" + + " rootInfo visits 100 winrate 0.6 ownership 0.25 -0.25"); + assertEquals(List.of("C3", "D4"), move.variation); + } + @Test void fromInfoParsesLeelaZeroLine() { MoveData move = diff --git a/src/test/java/featurecat/lizzie/analysis/PositionConfirmedRollbackTest.java b/src/test/java/featurecat/lizzie/analysis/PositionConfirmedRollbackTest.java index aebe8f3cf..6c5e75853 100644 --- a/src/test/java/featurecat/lizzie/analysis/PositionConfirmedRollbackTest.java +++ b/src/test/java/featurecat/lizzie/analysis/PositionConfirmedRollbackTest.java @@ -60,16 +60,16 @@ void acceptedRollbackDelayedResumeStartsOnceWithoutReplayingTheBoard() throws Ex } } - @Test + @org.junit.jupiter.api.RepeatedTest(10) void rejectedOrdinaryPredecessorCannotBeForgottenBySyncRollback() throws Exception { - try (Harness harness = Harness.open()) { - ExactSnapshotRestoreProtocolFixture.Transport transport = - ExactSnapshotRestoreProtocolFixture.install( + try (Harness harness = Harness.open(); + ExactSnapshotRestoreProtocolFixture.Transport transport = + ExactSnapshotRestoreProtocolFixture.installAsync( harness.engine, command -> command.equals("play W D4") ? null - : ExactSnapshotRestoreProtocolFixture.Response.success()); + : ExactSnapshotRestoreProtocolFixture.Response.success())) { harness.engine.playMoveNoPonder(Stone.BLACK, "Q16"); harness.engine.ponder(); awaitRawCommand(transport, "kata-analyze", 0); @@ -79,7 +79,7 @@ void rejectedOrdinaryPredecessorCannotBeForgottenBySyncRollback() throws Excepti String predecessor = awaitRawCommand(transport, "play W D4", 0); harness.acceptSnapshot(harness.p1); harness.frame.scheduledResume.run(); - harness.engine.processCommandResponseLineForTest( + transport.respond( "?" + predecessor.substring(0, predecessor.indexOf(' ')) + " rejected play"); long deadline = System.nanoTime() + OBSERVATION_TIMEOUT_NANOS; while (harness.engine.isLoaded @@ -342,7 +342,7 @@ void failedUndoWriteCannotResumeAnalysis() throws Exception { assertFailedSyncDoesNotResume("write"); } - @Test + @org.junit.jupiter.api.RepeatedTest(10) void timedOutUndoCannotBeRevivedByLateAckOrDelayedResume() throws Exception { assertFailedSyncDoesNotResume("timeout"); } @@ -351,8 +351,8 @@ private void assertFailedSyncDoesNotResume(String failure) throws Exception { try (Harness harness = Harness.open()) { harness.engine.requireResponseBeforeSend = false; ((PublicationControlledLeelaz) harness.engine).responseTimeoutMillis = 50; - ExactSnapshotRestoreProtocolFixture.Transport transport = - ExactSnapshotRestoreProtocolFixture.install( + try (ExactSnapshotRestoreProtocolFixture.Transport transport = + ExactSnapshotRestoreProtocolFixture.installAsync( harness.engine, command -> { if (command.equals("undo")) { @@ -363,21 +363,24 @@ private void assertFailedSyncDoesNotResume(String failure) throws Exception { return null; } return ExactSnapshotRestoreProtocolFixture.Response.success(); - }); - harness.acceptEmptySnapshot(); - String undo = awaitRawCommand(transport, "undo", 0); - harness.frame.scheduledResume.run(); - long deadline = System.nanoTime() + OBSERVATION_TIMEOUT_NANOS; - while (harness.engine.isLoaded && System.nanoTime() < deadline) Thread.sleep(5L); - assertFalse(harness.engine.isLoaded, "failed sync must retire its unavailable engine target"); - acknowledge(harness.engine, undo); - harness.frame.scheduledResume.run(); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - assertEquals(0, payloadCount(transport, "kata-analyze")); - harness.engine.parseAnalysisLineForTest(info(16_768, 0.335525)); - assertNoAnalysis(harness.p0.getData()); - assertSame(harness.p0, harness.board.getHistory().getCurrentHistoryNode()); - assertSame(harness.p1, harness.board.getHistory().getMainEnd()); + })) { + harness.acceptEmptySnapshot(); + String undo = awaitRawCommand(transport, "undo", 0); + harness.frame.scheduledResume.run(); + long deadline = System.nanoTime() + OBSERVATION_TIMEOUT_NANOS; + while (harness.engine.isLoaded && System.nanoTime() < deadline) Thread.sleep(5L); + assertFalse(harness.engine.isLoaded, "failed sync must retire its unavailable engine target"); + transport.respond("=" + undo.substring(0, undo.indexOf(' '))); + transport.awaitResponses(); + harness.frame.scheduledResume.run(); + transport.awaitResponses(); + javax.swing.SwingUtilities.invokeAndWait(() -> {}); + assertEquals(0, payloadCount(transport, "kata-analyze")); + harness.engine.parseAnalysisLineForTest(info(16_768, 0.335525)); + assertNoAnalysis(harness.p0.getData()); + assertSame(harness.p0, harness.board.getHistory().getCurrentHistoryNode()); + assertSame(harness.p1, harness.board.getHistory().getMainEnd()); + } } } diff --git a/src/test/java/featurecat/lizzie/analysis/ReadBoardEngineResumeTest.java b/src/test/java/featurecat/lizzie/analysis/ReadBoardEngineResumeTest.java index 110002b2a..e4bf37690 100644 --- a/src/test/java/featurecat/lizzie/analysis/ReadBoardEngineResumeTest.java +++ b/src/test/java/featurecat/lizzie/analysis/ReadBoardEngineResumeTest.java @@ -526,42 +526,6 @@ void forceRebuildResumesAutoPlayAfterRemoteChangesFollowingFailedLocalMove() thr } } - @Test - void oneMoveAutoPlayResumesOrdinaryAnalysisWhenTrackingStillOwnsTheStream() throws Exception { - try (EngineResumeHarness harness = - EngineResumeHarness.create(rootHistory(emptyStones(), true))) { - harness.frame.bothSync = true; - harness.leelaz.isKatago = true; - buildHistory(harness.board, placement(0, 0, Stone.BLACK)); - harness.leelaz.Pondering(); - Leelaz.TrackingStreamLeaseAcquisition acquisition = - harness.leelaz.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - assertFalse(dispatchTrackingLine(harness.leelaz, "=800000000")); - processTrackingCommandResponse(harness.leelaz, "=800000000"); - assertTrue(dispatchTrackingLine(harness.leelaz, "")); - assertTrue(acquisition.lease().isOwned()); - assertTrue(acquisition.lease().send("kata-analyze 10")); - assertTrue(dispatchTrackingLine(harness.leelaz, "=800000001")); - harness.leelaz.Pondering(); - harness.frame.isAnaPlayingAgainstLeelaz = true; - - harness.sync( - snapshot( - stones(placement(0, 0, Stone.BLACK), placement(1, 0, Stone.WHITE)), - Optional.of(new int[] {1, 0}), - Stone.WHITE)); - - assertEquals( - 1, - harness.leelaz.ponderCount, - "one-move auto-play must submit ordinary analysis when tracking still owns the only stream."); - - assertTrue(acquisition.lease().release()); - assertTrue(dispatchTrackingLine(harness.leelaz, "")); - assertTrue(dispatchTrackingLine(harness.leelaz, "=800000002")); - assertTrue(dispatchTrackingLine(harness.leelaz, "")); - } - } @Test void forceRebuildRegeneratesFailedEngineMoveWhenPlayingAgainstLeelaz() throws Exception { @@ -2566,23 +2530,12 @@ private static void invokePlacementFailedLine(ReadBoard readBoard) throws Except method.invoke(readBoard, "error place failed"); } - private static boolean dispatchTrackingLine(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } - private static boolean beginReadBoardGmaSessionForTest(Leelaz engine) throws Exception { Method method = Leelaz.class.getDeclaredMethod("beginReadBoardGmaSession"); method.setAccessible(true); return (boolean) method.invoke(engine); } - private static void processTrackingCommandResponse(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); - } private static void invokeClearFailedLocalMoveStateIfAutoPlaySideChanged( ReadBoard readBoard, Stone autoPlayColor) throws Exception { diff --git a/src/test/java/featurecat/lizzie/analysis/ReadBoardShutdownTest.java b/src/test/java/featurecat/lizzie/analysis/ReadBoardShutdownTest.java index 25a1bbbb1..3b7c79b8e 100644 --- a/src/test/java/featurecat/lizzie/analysis/ReadBoardShutdownTest.java +++ b/src/test/java/featurecat/lizzie/analysis/ReadBoardShutdownTest.java @@ -13,7 +13,6 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.ServerSocket; @@ -24,9 +23,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import javax.swing.SwingUtilities; import org.junit.jupiter.api.Test; @@ -237,75 +234,6 @@ public int read() throws java.io.IOException { } } - @Test - void retirementAndGmaActivationChooseOneDeterministicWinner() throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - LizzieFrame previousFrame = Lizzie.frame; - try { - BlockingGmaActivationLeelaz engine = new BlockingGmaActivationLeelaz(); - ReadBoard readBoard = initializedReadBoardForRetirement(); - LizzieFrame frame = allocate(LizzieFrame.class); - frame.readBoard = readBoard; - Lizzie.frame = frame; - Lizzie.leelaz = engine; - Object identity = armPendingGmaTarget(readBoard, 7L); - Leelaz.TrackingHandoffTarget target = newReadBoardGmaTarget(readBoard, identity, 7L); - AtomicReference activationFailure = new AtomicReference<>(); - AtomicReference shutdownFailure = new AtomicReference<>(); - Thread activationThread = - new Thread( - () -> { - try { - target.activate(successfulRetainedActivation()); - } catch (Throwable failure) { - activationFailure.set(failure); - } - }, - "readboard-gma-promotion-winner"); - activationThread.start(); - assertTrue(engine.activationEntered.await(1, TimeUnit.SECONDS)); - - Thread shutdownThread = - new Thread( - () -> { - try { - readBoard.shutdown(); - } catch (Throwable failure) { - shutdownFailure.set(failure); - } - }, - "readboard-gma-retirement-loser"); - shutdownThread.start(); - assertFalse(engine.retirementEntered.await(50, TimeUnit.MILLISECONDS)); - - engine.allowActivationToFinish.countDown(); - activationThread.join(1000L); - shutdownThread.join(1000L); - - assertNull(activationFailure.get()); - assertNull(shutdownFailure.get()); - assertEquals(1, engine.activationCount.get()); - assertEquals(1, engine.retirementCount.get()); - - ReadBoard retiredFirst = initializedReadBoardForRetirement(); - frame.readBoard = retiredFirst; - Object retiredIdentity = armPendingGmaTarget(retiredFirst, 8L); - Leelaz.TrackingHandoffTarget lateTarget = - newReadBoardGmaTarget(retiredFirst, retiredIdentity, 8L); - retiredFirst.shutdown(); - - lateTarget.activate(successfulRetainedActivation()); - - assertEquals( - 1, - engine.activationCount.get(), - "retirement winner must suppress late activation bytes."); - assertEquals(2, engine.retirementCount.get()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.frame = previousFrame; - } - } @Test void openBoardSyncDoesNotBlockEventDispatchThreadWhileRestarting() throws Exception { @@ -376,49 +304,6 @@ private static ReadBoard initializedReadBoardForRetirement() throws Exception { return readBoard; } - private static Object armPendingGmaTarget(ReadBoard readBoard, long generation) throws Exception { - Object identity = new Object(); - setField(readBoard, "trackingEligibilityIdentity", identity); - setField(readBoard, "readBoardGmaSessionGeneration", generation); - setField(readBoard, "readBoardGmaPending", true); - setField(readBoard, "readBoardGmaPendingIdentity", identity); - setField(readBoard, "readBoardGmaPendingGeneration", generation); - setField(readBoard, "readBoardGmaAutoPlayActive", true); - return identity; - } - - private static Leelaz.TrackingHandoffTarget newReadBoardGmaTarget( - ReadBoard readBoard, Object identity, long generation) throws Exception { - Class targetClass = - Class.forName("featurecat.lizzie.analysis.ReadBoard$ReadBoardGmaHandoffTarget"); - Constructor constructor = - targetClass.getDeclaredConstructor( - ReadBoard.class, - Object.class, - long.class, - String.class, - int.class, - int.class, - boolean.class); - constructor.setAccessible(true); - return (Leelaz.TrackingHandoffTarget) - constructor.newInstance(readBoard, identity, generation, "B", 1, 1, false); - } - - private static Leelaz.TrackingHandoffActivation successfulRetainedActivation() { - return new Leelaz.TrackingHandoffActivation() { - @Override - public boolean activateForegroundAnalysis( - java.util.function.Consumer lineConsumer, Runnable onClosed) { - return false; - } - - @Override - public boolean completeRetainedEngineMode() { - return true; - } - }; - } private static final class UnsafeHolder { private static final sun.misc.Unsafe UNSAFE = loadUnsafe(); @@ -496,57 +381,6 @@ void retireReadBoardGmaSession() { } } - private static final class BlockingGmaActivationLeelaz extends Leelaz { - private final CountDownLatch activationEntered = new CountDownLatch(1); - private final CountDownLatch allowActivationToFinish = new CountDownLatch(1); - private final CountDownLatch retirementEntered = new CountDownLatch(1); - private final AtomicInteger activationCount = new AtomicInteger(); - private final AtomicInteger retirementCount = new AtomicInteger(); - - private BlockingGmaActivationLeelaz() throws Exception { - super(""); - } - - @Override - void activateReadBoardGmaAfterTracking( - TrackingHandoffTarget target, - String color, - int maxTimeSeconds, - int maxVisits, - boolean ponder, - TrackingHandoffActivation activation) { - activateReadBoardGmaAfterTracking( - target, color, maxTimeSeconds, maxVisits, ponder, activation, null); - } - - @Override - void activateReadBoardGmaAfterTracking( - TrackingHandoffTarget target, - String color, - int maxTimeSeconds, - int maxVisits, - boolean ponder, - TrackingHandoffActivation activation, - Leelaz.ReadBoardGmaSessionAdmission sessionAdmission) { - activationCount.incrementAndGet(); - activationEntered.countDown(); - try { - if (!allowActivationToFinish.await(1, TimeUnit.SECONDS)) { - throw new AssertionError("timed out waiting to release activation"); - } - } catch (InterruptedException failure) { - Thread.currentThread().interrupt(); - throw new AssertionError(failure); - } - activation.completeRetainedEngineMode(); - } - - @Override - void retireReadBoardGmaSession() { - retirementCount.incrementAndGet(); - retirementEntered.countDown(); - } - } private static final class TrackingSocket extends Socket { private boolean closeCalled; diff --git a/src/test/java/featurecat/lizzie/analysis/ReadBoardSyncDecisionTest.java b/src/test/java/featurecat/lizzie/analysis/ReadBoardSyncDecisionTest.java index 177632556..eebc097df 100644 --- a/src/test/java/featurecat/lizzie/analysis/ReadBoardSyncDecisionTest.java +++ b/src/test/java/featurecat/lizzie/analysis/ReadBoardSyncDecisionTest.java @@ -2759,7 +2759,7 @@ void fixtureBuildersResetBoardSizeAndZobristBeforeCreating3x3Data() { } @Test - void acceptedCompleteFramePublishesStableTrackingEligibilityAndNextFrameInvalidatesFirst() + void pendingFrameClosesNewAdmissionWithoutInvalidatingAcceptedSemanticRequests() throws Exception { try (SyncHarness harness = SyncHarness.create(false, emptyHistory())) { harness.leelaz.enableReadBoardGmaSupport(); @@ -2777,82 +2777,11 @@ void acceptedCompleteFramePublishesStableTrackingEligibilityAndNextFrameInvalida ReadBoardTrackingEligibilityAdapter.Snapshot pending = harness.readBoard.snapshot(); assertEquals(ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING, pending.reason()); - assertTrue(pending.revision() > stable.revision()); - assertEquals(1, invalidations.get()); + assertFalse(pending.stable()); + assertEquals(0, invalidations.get()); } } - @Test - void trackingEligibilityExplainsFirstFramePendingLocalGmaRestoreAndNodeMismatch() - throws Exception { - try (SyncHarness harness = SyncHarness.create(false, emptyHistory())) { - harness.leelaz.enableReadBoardGmaSupport(); - harness.sync(snapshot(stones(), Optional.empty(), Stone.EMPTY)); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.STABLE, harness.readBoard.snapshot().reason()); - - setField(harness.readBoard, "awaitingFirstSyncFrame", true); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.FIRST_FRAME, - harness.readBoard.snapshot().reason()); - setField(harness.readBoard, "awaitingFirstSyncFrame", false); - - harness.readBoard.lastMovePlayByLizzie = true; - setField(harness.readBoard, "waitingForReadBoardLocalMoveAck", true); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.PENDING_LOCAL_MOVE, - harness.readBoard.snapshot().reason()); - harness.readBoard.lastMovePlayByLizzie = false; - setField(harness.readBoard, "waitingForReadBoardLocalMoveAck", false); - - setField(harness.readBoard, "readBoardGmaPending", true); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.GMA, harness.readBoard.snapshot().reason()); - setField(harness.readBoard, "readBoardGmaPending", false); - - setField(harness.leelaz, "engineStateUnrestored", true); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.ENGINE_UNRESTORED, - harness.readBoard.snapshot().reason()); - setField(harness.leelaz, "engineStateUnrestored", false); - - setField(harness.board, "contextRevision", harness.board.getContextRevision() + 1L); - assertEquals( - ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH, - harness.readBoard.snapshot().reason()); - } - } - - @Test - void localNavigationAndHistoryOverwriteInvalidateStableEligibilityRevision() throws Exception { - try (SyncHarness harness = SyncHarness.create(false, emptyHistory())) { - harness.leelaz.enableReadBoardGmaSupport(); - harness.sync(snapshot(stones(), Optional.empty(), Stone.EMPTY)); - ReadBoardTrackingEligibilityAdapter.Snapshot first = harness.readBoard.snapshot(); - AtomicInteger invalidations = new AtomicInteger(); - harness.readBoard.observeInvalidation(first.identity(), invalidations::incrementAndGet); - setField( - harness.readBoard, "localNavigationTracker", new SyncLocalNavigationTracker(() -> true)); - - harness.readBoard.onLocalHistoryNavigation(); - - ReadBoardTrackingEligibilityAdapter.Snapshot navigated = harness.readBoard.snapshot(); - assertEquals(ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH, navigated.reason()); - assertTrue(navigated.revision() > first.revision()); - assertEquals(1, invalidations.get()); - - harness.sync(snapshot(stones(), Optional.empty(), Stone.EMPTY)); - ReadBoardTrackingEligibilityAdapter.Snapshot second = harness.readBoard.snapshot(); - harness.readBoard.observeInvalidation(second.identity(), invalidations::incrementAndGet); - - harness.readBoard.onHistoryOverwritten(); - - ReadBoardTrackingEligibilityAdapter.Snapshot overwritten = harness.readBoard.snapshot(); - assertEquals(ReadBoardTrackingEligibilityAdapter.Reason.NODE_MISMATCH, overwritten.reason()); - assertTrue(overwritten.revision() > second.revision()); - assertEquals(2, invalidations.get()); - } - } private static void armFoxMoveNumber(ReadBoard readBoard, int moveNumber) { readBoard.parseLine("syncPlatform fox"); diff --git a/src/test/java/featurecat/lizzie/analysis/Ticket07RestartBootstrapProductionEntryTest.java b/src/test/java/featurecat/lizzie/analysis/Ticket07RestartBootstrapProductionEntryTest.java index 13d15614e..5da093642 100644 --- a/src/test/java/featurecat/lizzie/analysis/Ticket07RestartBootstrapProductionEntryTest.java +++ b/src/test/java/featurecat/lizzie/analysis/Ticket07RestartBootstrapProductionEntryTest.java @@ -104,10 +104,7 @@ void publicRestartPublishesNewBindingAndStartupCrossesLifecycleGate() throws Exc setField(engine, "endGetCommandList", true); setField(engine, "outputStream", new BufferedOutputStream(new ByteArrayOutputStream())); - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); Object oldBinding = getField(engine, "readerStreamBinding"); - long oldIncarnation = engine.trackingStreamIncarnation(); - assertTrue(tracking.lease().isOwned(), "precondition: active tracking must be owned"); PhaseAEngineManager manager = new PhaseAEngineManager(List.of(engine)); try (WatchService watchService = FileSystems.getDefault().newWatchService()) { @@ -122,8 +119,6 @@ void publicRestartPublishesNewBindingAndStartupCrossesLifecycleGate() throws Exc assertNotNull(newProcess, "public restart must publish a new process"); assertTrue(newProcess.isAlive(), "new process must remain alive while startup is gated"); assertNotSame(oldBinding, newBinding, "restart must publish a new reader binding"); - assertTrue(engine.trackingStreamIncarnation() > oldIncarnation); - assertFalse(tracking.lease().isOwned(), "old tracking must retire at rebind"); assertFalse( engine.sendRawConsoleCommand("showboard"), "non-receipt commands must not enter the new restart binding"); @@ -135,14 +130,8 @@ void publicRestartPublishesNewBindingAndStartupCrossesLifecycleGate() throws Exc boolean lifecycleGate = (boolean) getField(engine, "exclusiveGtpLifecycleQueueGate"); assertTrue( markerCreated, - "startup name was queued but did not reach the new writer; oldIncarnation=" - + oldIncarnation - + ", newPid=" + "startup name was queued but did not reach the new writer; newPid=" + newPid - + ", newIncarnation=" - + engine.trackingStreamIncarnation() - + ", trackingOwned=" - + tracking.lease().isOwned() + ", lifecycleGate=" + lifecycleGate + ", queueHead=" @@ -275,19 +264,6 @@ private static int queueSize(Leelaz engine) throws Exception { } } - private static Leelaz.TrackingStreamLeaseAcquisition activateTracking(Leelaz engine) - throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - Method response = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - response.setAccessible(true); - response.invoke(engine, "=800000000"); - Method dispatch = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - dispatch.setAccessible(true); - assertTrue((boolean) dispatch.invoke(engine, "")); - assertTrue(tracking.lease().send("kata-analyze B 10")); - return tracking; - } private static void stopEngineForTest(Leelaz engine) { try { diff --git a/src/test/java/featurecat/lizzie/analysis/TrackingAnalysisControllerTest.java b/src/test/java/featurecat/lizzie/analysis/TrackingAnalysisControllerTest.java index d8105fb17..960eca7d2 100644 --- a/src/test/java/featurecat/lizzie/analysis/TrackingAnalysisControllerTest.java +++ b/src/test/java/featurecat/lizzie/analysis/TrackingAnalysisControllerTest.java @@ -1,9 +1,6 @@ package featurecat.lizzie.analysis; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import featurecat.lizzie.Config; @@ -13,887 +10,40 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; class TrackingAnalysisControllerTest { - - @Test - void addPointAcquiresLeaseAndSendsConstrainedAnalyzeAfterInitialFence() throws Exception { - try (TestState state = TestState.open()) { - ManualTimeoutScheduler scheduler = new ManualTimeoutScheduler(); - TrackingAnalysisController controller = new TrackingAnalysisController(scheduler); - TrackingAnalysisController.Context context = - new TrackingAnalysisController.Context( - new Object(), - new Object(), - 19, - 19, - "stones", - true, - "chinese", - 7.5, - state.engine, - state.engine.trackingStreamIncarnation(), - new TrackingAnalysisController.Parameters(10, 100), - null); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("d4", context)); - assertEquals("800000000 stop\n", state.commands()); - - completeInitialFence(state.engine); - - assertEquals( - "800000000 stop\n" + "800000001 kata-analyze 10 allow B D4 1 allow W D4 1\n", - state.commands()); - assertEquals(List.of("D4"), new ArrayList<>(controller.snapshot().selectedPoints())); - assertTrue(controller.snapshot().active()); - assertEquals(1, scheduler.pendingCount()); - } - } - - @Test - void readBoardAdapterRevalidatesEligibilityBeforeSendingTrackingRequest() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - MutableEligibilitySource source = - new MutableEligibilitySource(state.output, 1L, state.output, 7L, true); - ReadBoardTrackingEligibilityAdapter adapter = - new ReadBoardTrackingEligibilityAdapter(controller, source); - TrackingAnalysisController.Context context = - state.contextWithReadBoard( - new TrackingAnalysisController.ReadBoardContext(state.output, 1L, state.output, 7L)); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, adapter.addPoint("D4", context)); - source.publish(state.output, 2L, state.output, 8L, false); - completeInitialFence(state.engine); - - assertFalse(state.commands().contains("kata-analyze")); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertFalse(controller.snapshot().active()); - } - } - - @Test - void readBoardAdapterRejectsUnstableSnapshotWithoutAcquiringOrRetrying() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - MutableEligibilitySource source = - new MutableEligibilitySource(state.output, 1L, state.output, 7L, false); - ReadBoardTrackingEligibilityAdapter adapter = - new ReadBoardTrackingEligibilityAdapter(controller, source); - - assertEquals( - TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, - adapter.addPoint( - "D4", - state.contextWithReadBoard( - new TrackingAnalysisController.ReadBoardContext( - state.output, 1L, state.output, 7L)))); - - assertEquals("", state.commands()); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - } - } - - @Test - void pendingPointsRunNewestFirstAndRemovingPendingDoesNotInterruptCurrent() throws Exception { - try (TestState state = TestState.open()) { - ManualTimeoutScheduler scheduler = new ManualTimeoutScheduler(); - TrackingAnalysisController controller = new TrackingAnalysisController(scheduler); - TrackingAnalysisController.Context context = state.context(); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("F6", context)); - assertTrue(controller.removePoint("E5")); - assertEquals(List.of("D4", "F6"), new ArrayList<>(controller.snapshot().selectedPoints())); - - assertTrue(dispatch(state.engine, "info move D4 visits 100 winrate 0.55 pv D4 Q16")); - completeFinalFence(state.engine, 800000002); - assertTrue(state.commands().endsWith("800000003 stop\n")); - completeInitialFence(state.engine, 800000003); - - assertTrue( - state.commands().endsWith("800000004 kata-analyze 10 allow B F6 1 allow W F6 1\n")); - assertEquals(100, controller.snapshot().results().get("D4").visits()); - assertTrue(controller.snapshot().results().get("D4").completed()); - assertFalse(controller.snapshot().results().containsKey("E5")); - } - } - - @Test - void contextChangeClearsDisplayAndStaleCallbacksCannotRestoreIt() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - engine.Pondering(); - ManualTimeoutScheduler scheduler = new ManualTimeoutScheduler(); - TrackingAnalysisController controller = new TrackingAnalysisController(scheduler); - TrackingAnalysisController.Context original = state.context(); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", original)); - completeInitialFence(state.engine, 800000000); - assertTrue(dispatch(state.engine, "info move D4 visits 40 winrate 0.51 pv D4")); - ManualTimeoutScheduler.ManualTimeout staleTimeout = scheduler.latest(); - assertEquals(40, controller.snapshot().results().get("D4").visits()); - - controller.contextChanged(state.contextWithStones("changed")); - - assertTrue( - controller.snapshot().selectedPoints().isEmpty(), - controller.snapshot().selectedPoints() + " commands=" + state.commands()); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().active()); - assertTrue(state.commands().endsWith("800000002 stop\n")); - - assertTrue(dispatch(state.engine, "info move D4 visits 100 winrate 0.99 pv D4")); - runTimeoutOnWorker(staleTimeout); - completeFinalFence(state.engine, 800000002); - - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().active()); - assertEquals(0, engine.ponderCount.get()); - } - } - - @Test - void multiPointNaturalCompletionHandsInitialPonderBackExactlyOnce() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - engine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", state.context())); - completeInitialFence(engine, 800000000); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", state.context())); - assertTrue(dispatch(engine, "info move D4 visits 100 winrate 0.55 pv D4")); - completeFinalFence(engine, 800000002); - completeInitialFence(engine, 800000003); - assertTrue(dispatch(engine, "info move E5 visits 100 winrate 0.56 pv E5")); - completeFinalFence(engine, 800000005); - - assertEquals(1, engine.ponderCount.get()); - assertEquals(100, controller.snapshot().results().get("D4").visits()); - assertEquals(100, controller.snapshot().results().get("E5").visits()); - assertFalse(controller.snapshot().active()); - } - } - - @Test - void analysisControlPauseKeepsTrackingRestoreFromResumingPonder() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - engine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", state.context())); - completeInitialFence(engine, 800000000); - - Method pause = LizzieFrame.class.getDeclaredMethod("pauseFromAnalysisControl"); - pause.setAccessible(true); - pause.invoke(Lizzie.frame); - - assertTrue(dispatch(engine, "info move D4 visits 100 winrate 0.55 pv D4")); - completeFinalFence(engine, 800000002); - - assertEquals( - 0, - engine.ponderCount.get(), - "analysis-control pause must keep tracking restore from sending ponder."); - } - } - - @Test - void safeOrdinaryReleaseFreezesThenOrdinaryUpgradeClearsWithoutReacquire() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - engine.Pondering(); - AtomicInteger displayChanges = new AtomicInteger(); - TrackingAnalysisController controller = - new TrackingAnalysisController( - new ManualTimeoutScheduler(), displayChanges::incrementAndGet); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - assertTrue(dispatch(state.engine, "info move D4 visits 40 winrate 0.51 pv D4")); - - int beforeFreeze = displayChanges.get(); - assertTrue(state.engine.sendRawConsoleCommand("version")); - - assertTrue(displayChanges.get() > beforeFreeze); - assertTrue(controller.snapshot().frozen()); - assertFalse(controller.snapshot().active()); - assertEquals(List.of("D4"), new ArrayList<>(controller.snapshot().selectedPoints())); - assertEquals(40, controller.snapshot().results().get("D4").visits()); - assertEquals( - TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, - controller.addPoint("F6", context)); - assertEquals(List.of("D4"), new ArrayList<>(controller.snapshot().selectedPoints())); - - int beforeClear = displayChanges.get(); - state.engine.sendCommand("boardsize 19"); - - assertTrue(displayChanges.get() > beforeClear); - assertFalse(controller.snapshot().frozen()); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - completeFinalFence(state.engine, 800000002); - assertFalse(state.commands().contains("allow B E5")); - - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - assertEquals(0, engine.ponderCount.get()); - } - } - - @Test - void ponderHandbackRejectsStaleIncarnationLifecycleAndOrdinaryCompetition() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - Leelaz.TrackingStreamLeaseReceipt staleReceipt = - trackingReceipt(engine, engine.trackingStreamIncarnation() + 1L, true); - Leelaz.TrackingStreamLeaseReceipt currentReceipt = - trackingReceipt(engine, engine.trackingStreamIncarnation(), true); - - assertFalse(engine.restorePonderAfterTracking(staleReceipt)); - setField(engine, "exclusiveGtpLifecycleTransition", true); - assertFalse(engine.restorePonderAfterTracking(currentReceipt)); - setField(engine, "exclusiveGtpLifecycleTransition", false); - setField(engine, "normalCommandSendInProgress", true); - assertFalse(engine.restorePonderAfterTracking(currentReceipt)); - setField(engine, "normalCommandSendInProgress", false); - - assertEquals(0, engine.ponderCount.get()); - } - } - - @Test - void ponderHandbackFailureReturnsFalseWithoutEscaping() throws Exception { - ThrowingPonderLeelaz engine = new ThrowingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - Leelaz.TrackingStreamLeaseReceipt receipt = - trackingReceipt(engine, engine.trackingStreamIncarnation(), true); - - assertFalse(engine.restorePonderAfterTracking(receipt)); - assertEquals(1, engine.ponderCount.get()); - } - } - - @Test - void duplicateIllegalAndMismatchedAddsAreRejectedWithoutChangingCurrent() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - - assertEquals( - TrackingAnalysisController.AddResult.DUPLICATE, controller.addPoint("d4", context)); - assertEquals( - TrackingAnalysisController.AddResult.ILLEGAL, controller.addPoint("(1,2)", context)); - assertEquals( - TrackingAnalysisController.AddResult.ILLEGAL, controller.addPoint("I4", context)); - assertEquals( - TrackingAnalysisController.AddResult.ILLEGAL, controller.addPoint("T20", context)); - assertEquals( - TrackingAnalysisController.AddResult.CONTEXT_MISMATCH, - controller.addPoint("E5", state.contextWithStones("other"))); - - assertEquals(List.of("D4"), new ArrayList<>(controller.snapshot().selectedPoints())); - assertEquals("800000000 stop\n", state.commands()); - } - } - - @Test - void onlyStrictTargetProgressRenewsTimeoutAndStaleTimerCannotRelease() throws Exception { - try (TestState state = TestState.open()) { - ManualTimeoutScheduler scheduler = new ManualTimeoutScheduler(); - TrackingAnalysisController controller = new TrackingAnalysisController(scheduler); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - ManualTimeoutScheduler.ManualTimeout initialTimeout = scheduler.latest(); - - assertTrue(dispatch(state.engine, "info move Q16 visits 50 winrate 0.6 pv Q16")); - assertEquals(1, scheduler.totalCount()); - assertTrue(dispatch(state.engine, "info move D4 visits 10 winrate 0.51 pv D4")); - ManualTimeoutScheduler.ManualTimeout firstProgressTimeout = scheduler.latest(); - assertTrue(dispatch(state.engine, "info move D4 visits 10 winrate 0.52 pv D4")); - assertTrue(dispatch(state.engine, "info move D4 visits 9 winrate 0.53 pv D4")); - assertEquals(2, scheduler.totalCount()); - assertTrue(initialTimeout.cancelled); - - assertTrue(dispatch(state.engine, "info move D4 visits 20 winrate 0.54 pv D4")); - ManualTimeoutScheduler.ManualTimeout currentTimeout = scheduler.latest(); - assertTrue(firstProgressTimeout.cancelled); - runTimeoutOnWorker(firstProgressTimeout); - assertFalse(state.commands().contains("800000002 stop")); - - runTimeoutOnWorker(currentTimeout); - assertTrue(state.commands().endsWith("800000002 stop\n")); - completeFinalFence(state.engine, 800000002); - assertTrue(state.commands().endsWith("800000003 stop\n")); - assertFalse(controller.snapshot().results().containsKey("D4")); - } - } - - @Test - void newTrackingStartReplacesAClosedFrozenSnapshot() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertTrue(dispatch(state.engine, "info move D4 visits 40 winrate 0.51 pv D4")); - assertTrue(state.engine.sendRawConsoleCommand("version")); - completeFinalFence(state.engine, 800000002); - assertTrue(controller.snapshot().frozen()); - state.engine.sendCommand("boardsize 19"); - assertTrue(controller.snapshot().frozen()); - assertEquals(List.of("D4"), new ArrayList<>(controller.snapshot().selectedPoints())); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - - assertEquals(List.of("E5"), new ArrayList<>(controller.snapshot().selectedPoints())); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().frozen()); - assertTrue(state.commands().endsWith("800000003 stop\n")); - } - } - - @Test - void initialAndFinalFenceErrorsDropSchedulingStateWithoutPonderHandback() throws Exception { - RecordingPonderLeelaz initialErrorEngine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(initialErrorEngine)) { - initialErrorEngine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - - assertTrue(dispatch(initialErrorEngine, "?800000000 cannot stop")); - - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - assertEquals(0, initialErrorEngine.ponderCount.get()); - } - - RecordingPonderLeelaz finalErrorEngine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(finalErrorEngine)) { - finalErrorEngine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(finalErrorEngine, 800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - assertTrue(dispatch(finalErrorEngine, "info move D4 visits 100 winrate 0.55 pv D4")); - - assertTrue(dispatch(finalErrorEngine, "")); - assertTrue(dispatch(finalErrorEngine, "?800000002 cannot stop")); - - assertTrue( - controller.snapshot().selectedPoints().isEmpty(), - controller.snapshot().selectedPoints() + " commands=" + state.commands()); - assertTrue(controller.snapshot().results().isEmpty()); - assertEquals(0, finalErrorEngine.ponderCount.get()); - } - } - - @Test - void displaySnapshotCarriesItsImmutableContextAndResultValues() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertTrue( - dispatch( - state.engine, - "info move D4 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv D4 Q16")); - TrackingAnalysisController.DisplaySnapshot snapshot = controller.snapshot(); - - assertSame(context, snapshot.context()); - assertTrue(snapshot.generation() > 0L); - assertSame(context.displayNodeIdentity(), snapshot.context().displayNodeIdentity()); - assertEquals(40, snapshot.results().get("D4").visits()); - assertEquals(51.0, snapshot.results().get("D4").winrate()); - assertEquals(2.5, snapshot.results().get("D4").scoreLead()); - assertFalse(snapshot.results().get("D4").completed()); - assertThrows( - UnsupportedOperationException.class, () -> snapshot.selectedPoints().remove("D4")); - assertThrows(UnsupportedOperationException.class, () -> snapshot.results().clear()); - } - } - - @ParameterizedTest - @ValueSource( - strings = { - "history", - "node", - "width", - "height", - "stones", - "to-play", - "rules", - "komi", - "engine", - "interval", - "visits", - "readboard" - }) - void everyContextComponentInvalidatesCurrentTracking(String component) throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", state.context())); - completeInitialFence(state.engine, 800000000); - - controller.contextChanged(state.contextVariant(component)); - - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().active()); - assertTrue(state.commands().endsWith("800000002 stop\n")); - completeFinalFence(state.engine, 800000002); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - } - } - @Test - void unavailableContextInvalidatesWithoutRestoringPriorPonder() throws Exception { - try (TestState state = TestState.open()) { - state.engine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", state.context())); - completeInitialFence(state.engine, 800000000); - - controller.contextChanged(null); - completeFinalFence(state.engine, 800000002); - - assertFalse(state.engine.isPondering()); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - } - } - - @Test - void productionCutoverReferencesControllerAndHasNoLegacyTrackingRuntime() throws Exception { - Path productionRoot = Path.of("src/main/java"); - List productionReferences; - try (java.util.stream.Stream files = Files.walk(productionRoot)) { - productionReferences = - files - .filter(path -> path.toString().endsWith(".java")) - .filter( - path -> !path.getFileName().toString().equals("TrackingAnalysisController.java")) - .filter( - path -> { - try { - return Files.readString(path).contains("TrackingAnalysisController"); - } catch (java.io.IOException failure) { - throw new java.io.UncheckedIOException(failure); - } - }) - .toList(); - } - String rightClick = - Files.readString( - Path.of("src/main/java/featurecat/lizzie/gui/RightClickMenu.java"), - StandardCharsets.UTF_8); - String controllerSource = - Files.readString( - Path.of("src/main/java/featurecat/lizzie/analysis/TrackingAnalysisController.java"), - StandardCharsets.UTF_8); - - assertTrue( - productionReferences.contains( - Path.of( - "src/main/java/featurecat/lizzie/analysis/ReadBoardTrackingEligibilityAdapter.java"))); - assertTrue( - productionReferences.contains( - Path.of("src/main/java/featurecat/lizzie/gui/LizzieFrame.java"))); - assertTrue( - productionReferences.contains( - Path.of("src/main/java/featurecat/lizzie/gui/BoardRenderer.java"))); - assertTrue(rightClick.contains("addTrackingPoint(")); - assertTrue(rightClick.contains("removeTrackingPoint(")); - assertFalse(rightClick.contains("ensureTrackingEngineWithWarning()")); - assertFalse(rightClick.contains("triggerTrackingAnalysis()")); - assertFalse( - Files.exists(Path.of("src/main/java/featurecat/lizzie/analysis/TrackingEngine.java"))); - assertFalse( - Files.exists(Path.of("src/main/java/featurecat/lizzie/gui/TrackingConsolePane.java"))); - assertFalse(controllerSource.contains("AnalysisRequestBuilder")); - assertFalse(controllerSource.contains("Lizzie.board")); - assertFalse(controllerSource.contains("BoardHistory")); - assertFalse(controllerSource.contains("ReadBoard.")); - } - - @Test - void removingCurrentHidesItImmediatelyAndClearDropsAllPendingIntent() throws Exception { - try (TestState state = TestState.open()) { - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - TrackingAnalysisController.Context context = state.context(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - completeInitialFence(state.engine, 800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("E5", context)); - assertTrue(dispatch(state.engine, "info move D4 visits 40 winrate 0.51 pv D4")); - - assertTrue(controller.removePoint("D4")); - - assertEquals(List.of("E5"), new ArrayList<>(controller.snapshot().selectedPoints())); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().active()); - completeFinalFence(state.engine, 800000002); - completeInitialFence(state.engine, 800000003); - - controller.clear(); - - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(controller.snapshot().results().isEmpty()); - assertFalse(controller.snapshot().active()); - assertEquals( - TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, - controller.addPoint("F6", context)); - assertTrue(state.commands().endsWith("800000005 stop\n")); - completeFinalFence(state.engine, 800000005); - assertFalse(state.commands().contains("800000006 stop")); - } - } - - @Test - void typedHandoffClearsDisplayBeforeActivationAndPreventsPonderHandback() throws Exception { - RecordingPonderLeelaz engine = new RecordingPonderLeelaz(); - try (TestState state = TestState.open(engine)) { - engine.Pondering(); - TrackingAnalysisController controller = - new TrackingAnalysisController(new ManualTimeoutScheduler()); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", state.context())); - completeInitialFence(engine, 800000000); - assertTrue(dispatch(engine, "info move D4 visits 40 winrate 0.51 pv D4")); - AtomicInteger activations = new AtomicInteger(); - AtomicReference displayClearedAtActivation = new AtomicReference<>(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - displayClearedAtActivation.set(controller.snapshot().selectedPoints().isEmpty()); - if (activation.completeRetainedEngineMode()) { - activations.incrementAndGet(); - } - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) {} - }; - - Leelaz.TrackingHandoffClaim claim = engine.claimTrackingHandoff(target); - - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - assertTrue(dispatch(engine, "")); - assertTrue(dispatch(engine, "=800000002")); - assertTrue(dispatch(engine, "")); - assertEquals(1, activations.get()); - assertEquals(Boolean.TRUE, displayClearedAtActivation.get()); - assertEquals(0, engine.ponderCount.get()); - } - } - - private static void completeInitialFence(Leelaz engine) throws Exception { - completeInitialFence(engine, 800000000); - } - - private static void runTimeoutOnWorker(ManualTimeoutScheduler.ManualTimeout timeout) - throws Exception { - AtomicReference failure = new AtomicReference<>(); - Thread worker = - new Thread( - () -> { - try { - timeout.runEvenIfCancelled(); - } catch (Throwable throwable) { - failure.set(throwable); - } - }, - "tracking-controller-timeout-test"); - worker.setDaemon(true); - worker.start(); - worker.join(1000L); - assertFalse(worker.isAlive()); - assertEquals(null, failure.get()); - } - - private static void completeInitialFence(Leelaz engine, int commandId) throws Exception { - assertFalse(dispatch(engine, "=" + commandId)); - processCommandResponse(engine, "=" + commandId); - assertTrue(dispatch(engine, "")); - } - - private static void completeFinalFence(Leelaz engine, int commandId) throws Exception { - assertTrue(dispatch(engine, "")); - assertTrue(dispatch(engine, "=" + commandId)); - assertTrue(dispatch(engine, "")); - } - - private static boolean dispatch(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } - - private static void processCommandResponse(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); - } - - private static void closeExclusiveSessionForTest(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("exclusiveGtpSession"); - field.setAccessible(true); - Object session = field.get(engine); - if (session == null) { - return; - } - Method cancelInitial = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpInitialStopTimeout", session.getClass()); - cancelInitial.setAccessible(true); - cancelInitial.invoke(engine, session); - Method cancelRelease = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpReleaseStopTimeout", session.getClass()); - cancelRelease.setAccessible(true); - cancelRelease.invoke(engine, session); - Method close = Leelaz.class.getDeclaredMethod("closeExclusiveGtpSession", session.getClass()); - close.setAccessible(true); - close.invoke(engine, session); - } - - private static Leelaz.TrackingStreamLeaseReceipt trackingReceipt( - Leelaz engine, long incarnation, boolean wasPondering) throws Exception { - java.lang.reflect.Constructor constructor = - Leelaz.TrackingStreamLeaseReceipt.class.getDeclaredConstructor( - Leelaz.class, long.class, boolean.class); - constructor.setAccessible(true); - return constructor.newInstance(engine, incarnation, wasPondering); - } - - private static void setField(Object target, String name, Object value) throws Exception { - Field field = Leelaz.class.getDeclaredField(name); - field.setAccessible(true); - field.set(target, value); - } - - private static final class ManualTimeoutScheduler - implements TrackingAnalysisController.TimeoutScheduler { - private final List pending = new ArrayList<>(); - - @Override - public TrackingAnalysisController.Cancellable schedule(long delayMillis, Runnable task) { - ManualTimeout timeout = new ManualTimeout(task); - pending.add(timeout); - return timeout; - } - - int pendingCount() { - return (int) pending.stream().filter(timeout -> !timeout.cancelled).count(); - } - - int totalCount() { - return pending.size(); - } - - ManualTimeout latest() { - return pending.get(pending.size() - 1); - } - - static final class ManualTimeout implements TrackingAnalysisController.Cancellable { - private final Runnable task; - private boolean cancelled; - - private ManualTimeout(Runnable task) { - this.task = task; - } - - @Override - public void cancel() { - cancelled = true; - } - - void runEvenIfCancelled() { - task.run(); - } - } - } - - private static final class TestState implements AutoCloseable { - private final Leelaz previousEngine; - private final Board previousBoard; - private final Config previousConfig; - private final LizzieFrame previousFrame; - private final Leelaz engine; - private final ByteArrayOutputStream output; - - private TestState( - Leelaz previousEngine, - Board previousBoard, - Config previousConfig, - LizzieFrame previousFrame, - Leelaz engine, - ByteArrayOutputStream output) { - this.previousEngine = previousEngine; - this.previousBoard = previousBoard; - this.previousConfig = previousConfig; - this.previousFrame = previousFrame; - this.engine = engine; - this.output = output; - } - - static TestState open() throws Exception { - return open(new Leelaz("")); - } - - static TestState open(Leelaz engine) throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - Board previousBoard = Lizzie.board; - Config previousConfig = Lizzie.config; - LizzieFrame previousFrame = Lizzie.frame; + void unknownFocusCapabilityRejectsUserRequestWithoutStartingAnAnalysis() throws Exception { + Leelaz previousEngine = Lizzie.leelaz; + Board previousBoard = Lizzie.board; + Config previousConfig = Lizzie.config; + LizzieFrame previousFrame = Lizzie.frame; + try { + Leelaz engine = new Leelaz(""); engine.isLoaded = true; engine.started = true; engine.isKatago = true; engine.commandLists.addAll(List.of("stop", "kata-analyze")); - Field capabilityDiscovery = Leelaz.class.getDeclaredField("endGetCommandList"); - capabilityDiscovery.setAccessible(true); - capabilityDiscovery.set(engine, true); + setField(engine, "endGetCommandList", true); ByteArrayOutputStream output = new ByteArrayOutputStream(); - Field outputStream = Leelaz.class.getDeclaredField("outputStream"); - outputStream.setAccessible(true); - outputStream.set(engine, new BufferedOutputStream(output)); + setField(engine, "outputStream", new BufferedOutputStream(output)); Lizzie.leelaz = engine; - Lizzie.board = null; Lizzie.config = allocate(Config.class); Lizzie.frame = allocate(LizzieFrame.class); - return new TestState( - previousEngine, previousBoard, previousConfig, previousFrame, engine, output); - } - - TrackingAnalysisController.Context context() { - return contextWithStones("stones"); - } - - TrackingAnalysisController.Context contextWithStones(String stones) { - return new TrackingAnalysisController.Context( - this, - output, - 19, - 19, - stones, - true, - "chinese", - 7.5, - engine, - engine.trackingStreamIncarnation(), - new TrackingAnalysisController.Parameters(10, 100), - null); - } - - TrackingAnalysisController.Context contextWithReadBoard( - TrackingAnalysisController.ReadBoardContext readBoardContext) { - return new TrackingAnalysisController.Context( - this, - output, - 19, - 19, - "stones", - true, - "chinese", - 7.5, - engine, - engine.trackingStreamIncarnation(), - new TrackingAnalysisController.Parameters(10, 100), - readBoardContext); - } - - TrackingAnalysisController.Context contextVariant(String component) throws Exception { - Object history = component.equals("history") ? new Object() : this; - Object node = component.equals("node") ? new Object() : output; - int width = component.equals("width") ? 13 : 19; - int height = component.equals("height") ? 13 : 19; - String stones = component.equals("stones") ? "other" : "stones"; - boolean blackToPlay = !component.equals("to-play"); - String rules = component.equals("rules") ? "japanese" : "chinese"; - double komi = component.equals("komi") ? 6.5 : 7.5; - Leelaz contextEngine = component.equals("engine") ? new Leelaz("") : engine; - long incarnation = - component.equals("engine") - ? contextEngine.trackingStreamIncarnation() - : engine.trackingStreamIncarnation(); - TrackingAnalysisController.Parameters parameters = - new TrackingAnalysisController.Parameters( - component.equals("interval") ? 20 : 10, component.equals("visits") ? 200 : 100); - TrackingAnalysisController.ReadBoardContext readBoard = - component.equals("readboard") - ? new TrackingAnalysisController.ReadBoardContext(new Object(), 1L, new Object()) - : null; - return new TrackingAnalysisController.Context( - history, - node, - width, - height, - stones, - blackToPlay, - rules, - komi, - contextEngine, - incarnation, - parameters, - readBoard); - } - - String commands() { - return output.toString(StandardCharsets.UTF_8); - } - - @Override - public void close() throws Exception { - closeExclusiveSessionForTest(engine); + Lizzie.board = null; + TrackingAnalysisController controller = new TrackingAnalysisController(); + TrackingAnalysisController.Context context = new TrackingAnalysisController.Context( + this, output, 19, 19, "stones", true, "chinese", 7.5, + engine, engine.trackingStreamIncarnation(), + new TrackingAnalysisController.Parameters(10, 100), null); + assertEquals(TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, + controller.addPoint("D4", context)); + assertEquals("", output.toString(StandardCharsets.UTF_8)); + assertTrue(controller.snapshot().selectedPoints().isEmpty()); + } finally { Lizzie.leelaz = previousEngine; Lizzie.board = previousBoard; Lizzie.config = previousConfig; @@ -901,70 +51,10 @@ public void close() throws Exception { } } - private static final class MutableEligibilitySource - implements ReadBoardTrackingEligibilityAdapter.EligibilitySource { - private ReadBoardTrackingEligibilityAdapter.Snapshot snapshot; - private Runnable invalidationListener; - - private MutableEligibilitySource( - Object identity, long revision, Object node, long boardRevision, boolean stable) { - publish(identity, revision, node, boardRevision, stable); - } - - private void publish( - Object identity, long revision, Object node, long boardRevision, boolean stable) { - snapshot = - new ReadBoardTrackingEligibilityAdapter.Snapshot( - identity, - revision, - node, - boardRevision, - stable - ? ReadBoardTrackingEligibilityAdapter.Reason.STABLE - : ReadBoardTrackingEligibilityAdapter.Reason.FRAME_PENDING); - Runnable listener = invalidationListener; - if (listener != null && !stable) { - listener.run(); - } - } - - @Override - public ReadBoardTrackingEligibilityAdapter.Snapshot snapshot() { - return snapshot; - } - - @Override - public void observeInvalidation(Object identity, Runnable listener) { - invalidationListener = listener; - } - } - - private static final class RecordingPonderLeelaz extends Leelaz { - private final AtomicInteger ponderCount = new AtomicInteger(); - - private RecordingPonderLeelaz() throws Exception { - super(""); - } - - @Override - public void ponder() { - ponderCount.incrementAndGet(); - Pondering(); - } - } - - private static final class ThrowingPonderLeelaz extends Leelaz { - private final AtomicInteger ponderCount = new AtomicInteger(); - - private ThrowingPonderLeelaz() throws Exception { - super(""); - } - - @Override - public void ponder() { - ponderCount.incrementAndGet(); - throw new IllegalStateException("controlled ponder failure"); - } + private static void setField(Object target, String name, Object value) throws Exception { + Field field = Leelaz.class.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); } @SuppressWarnings("unchecked") diff --git a/src/test/java/featurecat/lizzie/analysis/TrackingWindowsIntegrationHarnessTest.java b/src/test/java/featurecat/lizzie/analysis/TrackingWindowsIntegrationHarnessTest.java deleted file mode 100644 index bded70d6f..000000000 --- a/src/test/java/featurecat/lizzie/analysis/TrackingWindowsIntegrationHarnessTest.java +++ /dev/null @@ -1,747 +0,0 @@ -package featurecat.lizzie.analysis; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import featurecat.lizzie.Config; -import featurecat.lizzie.ExtraMode; -import featurecat.lizzie.Lizzie; -import featurecat.lizzie.gui.GtpConsolePane; -import featurecat.lizzie.util.Utils; -import java.awt.AWTEvent; -import java.awt.EventQueue; -import java.awt.Toolkit; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import javax.swing.SwingUtilities; -import org.json.JSONArray; -import org.json.JSONObject; -import org.junit.jupiter.api.Test; - -class TrackingWindowsIntegrationHarnessTest { - private static final int REAL_WARMUPS = 3; - private static final int REAL_SAMPLES = 30; - - @Test - void controlledTransportWritesRawMonotonicAcquisitionAndHandoffSamples() throws Exception { - try (ControlledTransport transport = ControlledTransport.open(); - AsyncThrowableCapture failures = AsyncThrowableCapture.install()) { - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - TrackingAnalysisController controller = new TrackingAnalysisController(); - TrackingAnalysisController.Context context = transport.context(); - long addRequested = System.nanoTime(); - Future add = - executor.submit(() -> controller.addPoint("D4", context)); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, failures.await(add)); - transport.completeInitialFence(800000000); - long initialFenceReady = System.nanoTime(); - assertTrue(transport.commands().contains("kata-analyze 10 allow B D4 1 allow W D4 1")); - - AtomicLong activated = new AtomicLong(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - activated.set(System.nanoTime()); - assertTrue(activation.completeRetainedEngineMode()); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - throw new AssertionError("controlled handoff failed: " + failure); - } - }; - long handoffClaimed = System.nanoTime(); - Leelaz.TrackingHandoffClaim claim = transport.engine.claimTrackingHandoff(target); - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - transport.completeFinalFence(800000002); - - assertTrue(activated.get() >= handoffClaimed); - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - writeRawSamples(initialFenceReady - addRequested, activated.get() - handoffClaimed, 0L); - failures.assertNoFailures(); - } finally { - executor.shutdownNow(); - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); - } - } - } - - @Test - void harnessReportsWorkerEdtAndExecutorThrowablesAndRestoresHandlers() throws Exception { - Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); - AsyncThrowableCapture failures = AsyncThrowableCapture.install(); - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - Thread worker = - new Thread( - () -> { - throw new IllegalStateException("controlled worker failure"); - }); - worker.start(); - worker.join(); - - CountDownLatch edtRan = new CountDownLatch(1); - EventQueue.invokeLater( - () -> { - edtRan.countDown(); - throw new IllegalArgumentException("controlled EDT failure"); - }); - assertTrue(edtRan.await(5, TimeUnit.SECONDS)); - SwingUtilities.invokeAndWait(() -> {}); - - Future future = - executor.submit( - () -> { - throw new UnsupportedOperationException("controlled executor failure"); - }); - failures.await(future); - - assertThrows(AssertionError.class, failures::assertNoFailures); - } finally { - executor.shutdownNow(); - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); - failures.close(); - } - assertSame(previous, Thread.getDefaultUncaughtExceptionHandler()); - } - - @Test - void realKataGoGateRunsThirtySamplesWhenCommandIsConfigured() throws Exception { - String command = - System.getProperty( - "tracking.real.command", System.getenv().getOrDefault("TRACKING_REAL_COMMAND", "")); - if (command.isBlank()) { - return; - } - try (AsyncThrowableCapture failures = AsyncThrowableCapture.install(); - RealKataGoTransport transport = RealKataGoTransport.open(command, failures)) { - List samples = new ArrayList<>(); - for (int index = 0; index < REAL_WARMUPS + REAL_SAMPLES; index++) { - TimingSample sample = transport.measure(index - REAL_WARMUPS + 1); - if (index >= REAL_WARMUPS) { - samples.add(sample); - } - } - - writeRealSamples(samples); - assertTimingGate(samples); - failures.assertNoFailures(); - } - } - - private static void writeRawSamples( - long acquisitionNanos, long handoffNanos, long targetOperationNanos) throws Exception { - Path outputDirectory = Path.of("target", "tracking-windows-harness"); - Files.createDirectories(outputDirectory); - Files.writeString( - outputDirectory.resolve("controlled-samples.csv"), - "sample,acquisition_ns,handoff_ns,target_operation_ns\n" - + "1," - + acquisitionNanos - + "," - + handoffNanos - + "," - + targetOperationNanos - + "\n", - StandardCharsets.UTF_8); - JSONObject sample = - new JSONObject() - .put("sample", 1) - .put("acquisition_ns", acquisitionNanos) - .put("handoff_ns", handoffNanos) - .put("target_operation_ns", targetOperationNanos); - Files.writeString( - outputDirectory.resolve("controlled-samples.json"), - new JSONArray().put(sample).toString(2) + "\n", - StandardCharsets.UTF_8); - } - - private static void writeRealSamples(List samples) throws Exception { - String configuredOutput = System.getProperty("tracking.real.output", ""); - Path outputDirectory = - configuredOutput.isBlank() - ? Path.of("target", "tracking-windows-harness") - : Path.of(configuredOutput); - Files.createDirectories(outputDirectory); - StringBuilder csv = - new StringBuilder( - "sample,acquisition_ns,handoff_ns,target_operation_ns,timeout,terminal_recovery,second_click\n"); - JSONArray rows = new JSONArray(); - for (TimingSample sample : samples) { - csv.append(sample.number) - .append(',') - .append(sample.acquisitionNanos) - .append(',') - .append(sample.handoffNanos) - .append(',') - .append(sample.targetOperationNanos) - .append(",0,0,0\n"); - rows.put(sample.toJson()); - } - JSONObject summary = - new JSONObject() - .put("warmups", REAL_WARMUPS) - .put("sample_count", samples.size()) - .put("acquisition", timingSummary(samples, true)) - .put("handoff", timingSummary(samples, false)) - .put("timeouts", 0) - .put("terminal_recoveries", 0) - .put("second_clicks", 0); - Files.writeString( - outputDirectory.resolve("real-samples.csv"), csv.toString(), StandardCharsets.UTF_8); - Files.writeString( - outputDirectory.resolve("real-samples.json"), - new JSONObject().put("samples", rows).put("summary", summary).toString(2) + "\n", - StandardCharsets.UTF_8); - } - - private static JSONObject timingSummary(List samples, boolean acquisition) { - List values = - samples.stream() - .map(sample -> acquisition ? sample.acquisitionNanos : sample.handoffNanos) - .sorted(Comparator.naturalOrder()) - .toList(); - return new JSONObject() - .put("p50_ns", nearestRank(values, 0.50)) - .put("p95_ns", nearestRank(values, 0.95)) - .put("max_ns", values.get(values.size() - 1)); - } - - private static long nearestRank(List sorted, double percentile) { - int index = Math.max(0, (int) Math.ceil(percentile * sorted.size()) - 1); - return sorted.get(index); - } - - private static void assertTimingGate(List samples) { - assertEquals(REAL_SAMPLES, samples.size()); - List acquisition = - samples.stream().map(sample -> sample.acquisitionNanos).sorted().toList(); - List handoff = samples.stream().map(sample -> sample.handoffNanos).sorted().toList(); - assertTrue(nearestRank(acquisition, 0.95) <= TimeUnit.SECONDS.toNanos(2)); - assertTrue(nearestRank(handoff, 0.95) <= TimeUnit.SECONDS.toNanos(2)); - assertTrue(acquisition.get(acquisition.size() - 1) <= TimeUnit.SECONDS.toNanos(4)); - assertTrue(handoff.get(handoff.size() - 1) <= TimeUnit.SECONDS.toNanos(4)); - } - - private static final class TimingSample { - private final int number; - private final long acquisitionNanos; - private final long handoffNanos; - private final long targetOperationNanos; - - private TimingSample( - int number, long acquisitionNanos, long handoffNanos, long targetOperationNanos) { - this.number = number; - this.acquisitionNanos = acquisitionNanos; - this.handoffNanos = handoffNanos; - this.targetOperationNanos = targetOperationNanos; - } - - private JSONObject toJson() { - return new JSONObject() - .put("sample", number) - .put("acquisition_ns", acquisitionNanos) - .put("handoff_ns", handoffNanos) - .put("target_operation_ns", targetOperationNanos) - .put("timeout", false) - .put("terminal_recovery", false) - .put("second_click", false); - } - } - - private static final class AsyncThrowableCapture implements AutoCloseable { - private final Thread.UncaughtExceptionHandler previousHandler; - private final CapturingEventQueue eventQueue; - private final List failures = Collections.synchronizedList(new ArrayList<>()); - private boolean closed; - - private AsyncThrowableCapture(Thread.UncaughtExceptionHandler previousHandler) { - this.previousHandler = previousHandler; - this.eventQueue = new CapturingEventQueue(failures); - } - - static AsyncThrowableCapture install() { - Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); - AsyncThrowableCapture capture = new AsyncThrowableCapture(previous); - Toolkit.getDefaultToolkit().getSystemEventQueue().push(capture.eventQueue); - Thread.setDefaultUncaughtExceptionHandler((thread, failure) -> capture.failures.add(failure)); - return capture; - } - - T await(Future future) throws InterruptedException { - try { - return future.get(); - } catch (ExecutionException failure) { - failures.add(failure.getCause()); - return null; - } - } - - T await(Future future, long timeout, TimeUnit unit) throws InterruptedException { - try { - return future.get(timeout, unit); - } catch (ExecutionException failure) { - failures.add(failure.getCause()); - } catch (java.util.concurrent.TimeoutException failure) { - failures.add(failure); - future.cancel(true); - } - return null; - } - - void assertNoFailures() { - synchronized (failures) { - if (!failures.isEmpty()) { - AssertionError error = - new AssertionError("captured asynchronous Throwable: " + failures.get(0)); - failures.forEach(error::addSuppressed); - throw error; - } - } - } - - @Override - public void close() { - if (closed) { - return; - } - closed = true; - Thread.setDefaultUncaughtExceptionHandler(previousHandler); - eventQueue.restore(); - } - } - - private static final class CapturingEventQueue extends EventQueue { - private final List failures; - - private CapturingEventQueue(List failures) { - this.failures = failures; - } - - @Override - protected void dispatchEvent(AWTEvent event) { - try { - super.dispatchEvent(event); - } catch (Throwable failure) { - failures.add(failure); - } - } - - private void restore() { - super.pop(); - } - } - - private static final class RealKataGoTransport implements AutoCloseable { - private final Leelaz previousEngine; - private final Config previousConfig; - private final GtpConsolePane previousGtpConsole; - private final boolean previousEmpty; - private final Leelaz engine; - private final Process process; - private final BufferedOutputStream output; - private final RecordingOutputStream recordingOutput; - private final Thread readerThread; - private final Thread stderrThread; - - private RealKataGoTransport( - Leelaz previousEngine, - Config previousConfig, - GtpConsolePane previousGtpConsole, - boolean previousEmpty, - Leelaz engine, - Process process, - BufferedOutputStream output, - RecordingOutputStream recordingOutput, - Thread readerThread, - Thread stderrThread) { - this.previousEngine = previousEngine; - this.previousConfig = previousConfig; - this.previousGtpConsole = previousGtpConsole; - this.previousEmpty = previousEmpty; - this.engine = engine; - this.process = process; - this.output = output; - this.recordingOutput = recordingOutput; - this.readerThread = readerThread; - this.stderrThread = stderrThread; - } - - static RealKataGoTransport open(String command, AsyncThrowableCapture failures) - throws Exception { - Leelaz previousEngine = Lizzie.leelaz; - Config previousConfig = Lizzie.config; - GtpConsolePane previousGtpConsole = Lizzie.gtpConsole; - boolean previousEmpty = EngineManager.isEmpty; - Config config = allocate(Config.class); - config.extraMode = ExtraMode.Normal; - config.autoCheckEngineAlive = false; - Lizzie.config = config; - Lizzie.gtpConsole = allocate(SilentGtpConsole.class); - EngineManager.isEmpty = false; - - Process process = new ProcessBuilder(Utils.splitCommand(command)).start(); - BufferedReader stdout = - new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); - BufferedReader stderr = - new BufferedReader( - new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)); - Thread stderrThread = new Thread(() -> drain(stderr), "tracking-real-katago-stderr"); - stderrThread.start(); - RecordingOutputStream recordingOutput = new RecordingOutputStream(process.getOutputStream()); - BufferedOutputStream output = new BufferedOutputStream(recordingOutput); - - ExecutorService warmupExecutor = Executors.newSingleThreadExecutor(); - try { - output.write("name\n".getBytes(StandardCharsets.UTF_8)); - output.flush(); - Future response = warmupExecutor.submit(() -> readGtpResponse(stdout)); - String name = failures.await(response, 180, TimeUnit.SECONDS); - assertTrue(name != null && name.startsWith("="), "real KataGo did not answer name"); - } finally { - warmupExecutor.shutdownNow(); - assertTrue(warmupExecutor.awaitTermination(5, TimeUnit.SECONDS)); - } - - Leelaz engine = new Leelaz(command); - engine.isLoaded = true; - engine.started = true; - engine.isKatago = true; - engine.commandLists.addAll(List.of("stop", "kata-analyze")); - setField(engine, "process", process); - setField(engine, "inputStream", stdout); - setField(engine, "outputStream", output); - setField(engine, "endGetCommandList", true); - Lizzie.leelaz = engine; - Thread readerThread = new Thread(() -> invokeReader(engine), "tracking-real-katago-stdout"); - readerThread.start(); - return new RealKataGoTransport( - previousEngine, - previousConfig, - previousGtpConsole, - previousEmpty, - engine, - process, - output, - recordingOutput, - readerThread, - stderrThread); - } - - TimingSample measure(int sampleNumber) throws Exception { - CountDownLatch analyzeWritten = recordingOutput.prepareAnalyze(); - TrackingAnalysisController controller = new TrackingAnalysisController(); - TrackingAnalysisController.Context context = - new TrackingAnalysisController.Context( - this, - new Object(), - 19, - 19, - "real-katago-stones", - true, - "chinese", - 7.5, - engine, - engine.trackingStreamIncarnation(), - new TrackingAnalysisController.Parameters(10, 1_000_000), - null); - long addRequested = System.nanoTime(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, controller.addPoint("D4", context)); - assertTrue(analyzeWritten.await(8, TimeUnit.SECONDS), "initial fence did not become ready"); - long initialFenceReady = System.nanoTime(); - - CountDownLatch activated = new CountDownLatch(1); - AtomicLong activatedAt = new AtomicLong(); - AtomicLong targetOperationNanos = new AtomicLong(); - AtomicReference targetFailure = new AtomicReference<>(); - Leelaz.TrackingHandoffTarget target = - new Leelaz.TrackingHandoffTarget() { - @Override - public Leelaz.TrackingHandoffKind kind() { - return Leelaz.TrackingHandoffKind.RETAINED_ENGINE_MODE; - } - - @Override - public boolean isCurrent() { - return true; - } - - @Override - public void activate(Leelaz.TrackingHandoffActivation activation) { - long start = System.nanoTime(); - activatedAt.set(start); - if (!activation.completeRetainedEngineMode()) { - targetFailure.set(new AssertionError("real handoff activation was rejected")); - } - targetOperationNanos.set(System.nanoTime() - start); - activated.countDown(); - } - - @Override - public void fail(Leelaz.TrackingHandoffFailure failure) { - targetFailure.set(new AssertionError("real handoff failed: " + failure)); - activated.countDown(); - } - }; - long handoffClaimed = System.nanoTime(); - Leelaz.TrackingHandoffClaim claim = engine.claimTrackingHandoff(target); - assertEquals(Leelaz.TrackingHandoffAvailability.ACCEPTED_PENDING, claim.availability()); - assertTrue(activated.await(8, TimeUnit.SECONDS), "final fence did not activate target"); - if (targetFailure.get() != null) { - throw new AssertionError(targetFailure.get()); - } - assertTrue(controller.snapshot().selectedPoints().isEmpty()); - return new TimingSample( - sampleNumber, - initialFenceReady - addRequested, - activatedAt.get() - handoffClaimed, - targetOperationNanos.get()); - } - - @Override - public void close() throws Exception { - try { - engine.isNormalEnd = true; - if (process.isAlive()) { - output.write("quit\n".getBytes(StandardCharsets.UTF_8)); - output.flush(); - if (!process.waitFor(10, TimeUnit.SECONDS)) { - process.destroyForcibly(); - assertTrue(process.waitFor(10, TimeUnit.SECONDS)); - } - } - readerThread.join(TimeUnit.SECONDS.toMillis(10)); - stderrThread.join(TimeUnit.SECONDS.toMillis(10)); - assertFalse(readerThread.isAlive()); - assertFalse(stderrThread.isAlive()); - } finally { - Lizzie.leelaz = previousEngine; - Lizzie.config = previousConfig; - Lizzie.gtpConsole = previousGtpConsole; - EngineManager.isEmpty = previousEmpty; - } - } - - private static String readGtpResponse(BufferedReader reader) throws IOException { - StringBuilder response = new StringBuilder(); - String line; - boolean started = false; - while ((line = reader.readLine()) != null) { - if (!started && (line.startsWith("=") || line.startsWith("?"))) { - started = true; - } - if (started) { - response.append(line).append('\n'); - if (line.isEmpty()) { - return response.toString(); - } - } - } - return response.toString(); - } - - private static void invokeReader(Leelaz engine) { - try { - Method method = Leelaz.class.getDeclaredMethod("read"); - method.setAccessible(true); - method.invoke(engine); - } catch (InvocationTargetException failure) { - throw new RuntimeException(failure.getCause()); - } catch (ReflectiveOperationException failure) { - throw new RuntimeException(failure); - } - } - - private static void drain(BufferedReader reader) { - try { - while (reader.readLine() != null) {} - } catch (IOException failure) { - throw new RuntimeException(failure); - } - } - } - - private static final class SilentGtpConsole extends GtpConsolePane { - private SilentGtpConsole() { - super(null); - } - - @Override - public boolean isVisible() { - return false; - } - } - - private static final class RecordingOutputStream extends OutputStream { - private final OutputStream delegate; - private final StringBuilder line = new StringBuilder(); - private CountDownLatch analyzeWritten; - - private RecordingOutputStream(OutputStream delegate) { - this.delegate = delegate; - } - - synchronized CountDownLatch prepareAnalyze() { - analyzeWritten = new CountDownLatch(1); - return analyzeWritten; - } - - @Override - public synchronized void write(int value) throws IOException { - delegate.write(value); - record(value); - } - - @Override - public synchronized void write(byte[] bytes, int offset, int length) throws IOException { - delegate.write(bytes, offset, length); - for (int index = offset; index < offset + length; index++) { - record(bytes[index]); - } - } - - @Override - public void flush() throws IOException { - delegate.flush(); - } - - private void record(int value) { - if (value == '\n') { - if (line.indexOf("kata-analyze") >= 0 && analyzeWritten != null) { - analyzeWritten.countDown(); - } - line.setLength(0); - } else if (value != '\r') { - line.append((char) (value & 0xff)); - } - } - } - - private static final class ControlledTransport implements AutoCloseable { - private final Leelaz previousEngine; - private final Leelaz engine; - private final ByteArrayOutputStream output; - - private ControlledTransport( - Leelaz previousEngine, Leelaz engine, ByteArrayOutputStream output) { - this.previousEngine = previousEngine; - this.engine = engine; - this.output = output; - } - - static ControlledTransport open() throws Exception { - Leelaz previous = Lizzie.leelaz; - Leelaz engine = new Leelaz(""); - engine.isLoaded = true; - engine.started = true; - engine.isKatago = true; - engine.commandLists.addAll(List.of("stop", "kata-analyze")); - setField(engine, "endGetCommandList", true); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - setField(engine, "outputStream", new BufferedOutputStream(output)); - Lizzie.leelaz = engine; - return new ControlledTransport(previous, engine, output); - } - - TrackingAnalysisController.Context context() { - return new TrackingAnalysisController.Context( - this, - output, - 19, - 19, - "controlled-stones", - true, - "chinese", - 7.5, - engine, - engine.trackingStreamIncarnation(), - new TrackingAnalysisController.Parameters(10, 100), - null); - } - - void completeInitialFence(int commandId) throws Exception { - assertFalse(dispatch("=" + commandId)); - processCommandResponse("=" + commandId); - assertTrue(dispatch("")); - } - - void completeFinalFence(int commandId) throws Exception { - assertTrue(dispatch("")); - assertTrue(dispatch("=" + commandId)); - assertTrue(dispatch("")); - } - - String commands() { - return output.toString(StandardCharsets.UTF_8); - } - - private boolean dispatch(String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } - - private void processCommandResponse(String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); - } - - @Override - public void close() { - Lizzie.leelaz = previousEngine; - } - } - - private static void setField(Leelaz engine, String name, Object value) throws Exception { - Field field = Leelaz.class.getDeclaredField(name); - field.setAccessible(true); - field.set(engine, value); - } - - @SuppressWarnings("unchecked") - private static T allocate(Class type) throws Exception { - Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); - field.setAccessible(true); - return (T) ((sun.misc.Unsafe) field.get(null)).allocateInstance(type); - } -} diff --git a/src/test/java/featurecat/lizzie/gui/LizzieFrameRegressionTest.java b/src/test/java/featurecat/lizzie/gui/LizzieFrameRegressionTest.java index a2630b6c3..689fa7ddf 100644 --- a/src/test/java/featurecat/lizzie/gui/LizzieFrameRegressionTest.java +++ b/src/test/java/featurecat/lizzie/gui/LizzieFrameRegressionTest.java @@ -38,8 +38,6 @@ import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -63,7 +61,6 @@ import javax.swing.SwingUtilities; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.StyleSheet; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.json.JSONObject; @@ -71,15 +68,6 @@ class LizzieFrameRegressionTest { private static final int BOARD_SIZE = 2; private static final int BOARD_AREA = BOARD_SIZE * BOARD_SIZE; - private final List createdTrackingEngines = new ArrayList<>(); - - @AfterEach - void closeCreatedTrackingSessions() throws Exception { - for (Leelaz engine : createdTrackingEngines) { - closeExclusiveSessionForTest(engine); - } - createdTrackingEngines.clear(); - } @Test void autoSaveFilesUseConfiguredWorkDirectoryInsteadOfProcessCwd(@TempDir Path workDir) { @@ -1095,168 +1083,6 @@ void noAnalyzeFallbackUsesHeldModeReservationAndRestoresPonderingWhenCancelled() } } - @Test - void continuePlayModeActivatesOnceOnlyAfterTrackingFence() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - RetainedModeActivationFrame frame = allocate(RetainedModeActivationFrame.class); - Leelaz engine = reusableTrackingKatago(); - ByteArrayOutputStream output = installOutput(engine); - installEmptyBoard(); - Lizzie.frame = frame; - Lizzie.leelaz = engine; - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - frame.continueAiPlaying(true, true, true, false); - - assertFalse(frame.isPlayingAgainstLeelaz); - assertEquals(0, frame.activations); - assertEquals(0, frame.conflicts); - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertEquals("800000000 stop\n", output.toString(java.nio.charset.StandardCharsets.UTF_8)); - - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - - assertTrue(frame.isPlayingAgainstLeelaz); - assertEquals(1, frame.activations); - assertEquals(0, frame.conflicts); - assertEquals(0, frame.activationFailures); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.PLAY_MODE, - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}).availability()); - } - } - - @Test - void newGameAndAnalyzeGameActivateOnlyAfterTheirTrackingFence() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - for (boolean newGame : List.of(true, false)) { - RetainedModeActivationFrame frame = allocate(RetainedModeActivationFrame.class); - Leelaz engine = reusableTrackingKatago(); - ByteArrayOutputStream output = installOutput(engine); - installEmptyBoard(); - Lizzie.frame = frame; - Lizzie.leelaz = engine; - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - if (newGame) { - frame.startNewGame(); - } else { - frame.startAnalyzeGameDialog(); - } - - assertEquals(0, frame.activations); - assertEquals("800000000 stop\n", output.toString(java.nio.charset.StandardCharsets.UTF_8)); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - assertEquals(1, frame.activations); - assertEquals(newGame ? "new-game" : "analyze-game", frame.lastAction); - assertTrue(frame.modeReservationAvailableDuringAction); - assertEquals(0, frame.conflicts); - assertEquals(0, frame.activationFailures); - } - } - } - - @Test - void secondRetainedModeEntryReportsBusyWithoutReplacingFirstTarget() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - RetainedModeActivationFrame frame = allocate(RetainedModeActivationFrame.class); - Leelaz engine = reusableTrackingKatago(); - installOutput(engine); - installEmptyBoard(); - Lizzie.frame = frame; - Lizzie.leelaz = engine; - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - frame.continueAiPlaying(true, true, true, false); - frame.startAnalyzeGameDialog(); - - assertEquals(1, frame.conflicts); - assertEquals(0, frame.activationFailures); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - assertEquals(1, frame.activations); - assertEquals("continue", frame.lastAction); - } - } - - @Test - void continuePlayModeDoesNotActivateAfterItsContextChanges() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - RetainedModeActivationFrame frame = allocate(RetainedModeActivationFrame.class); - Leelaz engine = reusableTrackingKatago(); - installOutput(engine); - installEmptyBoard(); - Lizzie.frame = frame; - Lizzie.leelaz = engine; - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - - frame.continueAiPlaying(true, true, true, false); - Lizzie.leelaz = reusableTrackingKatago(); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - - assertFalse(frame.isPlayingAgainstLeelaz); - assertEquals(0, frame.activations); - assertEquals(0, frame.conflicts); - assertEquals(1, frame.activationFailures); - } - } - - @Test - void retainedModeRevalidatesContextOnEdtImmediatelyBeforeMutation() throws Exception { - try (TestEnvironment env = TestEnvironment.open()) { - RetainedModeActivationFrame frame = allocate(RetainedModeActivationFrame.class); - Leelaz engine = reusableTrackingKatago(); - installOutput(engine); - installEmptyBoard(); - Lizzie.frame = frame; - Lizzie.leelaz = engine; - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - frame.continueAiPlaying(true, true, true, false); - - CountDownLatch edtBlocked = new CountDownLatch(1); - CountDownLatch releaseEdt = new CountDownLatch(1); - AtomicReference workerFailure = new AtomicReference<>(); - SwingUtilities.invokeLater( - () -> { - edtBlocked.countDown(); - try { - if (!releaseEdt.await(2, TimeUnit.SECONDS)) { - throw new IllegalStateException("timed out waiting to release EDT"); - } - } catch (InterruptedException failure) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(failure); - } - }); - assertTrue(edtBlocked.await(1, TimeUnit.SECONDS)); - - Thread fenceThread = - new Thread( - () -> { - try { - processCommandResponse(engine, "=800000000"); - dispatchExclusiveLine(engine, ""); - } catch (Throwable failure) { - workerFailure.set(failure); - } - }, - "retained-mode-edt-revalidation"); - fenceThread.start(); - Lizzie.board.getHistory().setStone(new int[] {0, 0}, Stone.BLACK); - releaseEdt.countDown(); - fenceThread.join(2000L); - - assertFalse(fenceThread.isAlive()); - assertEquals(null, workerFailure.get()); - assertEquals(0, frame.activations); - assertEquals(0, frame.conflicts); - assertEquals(1, frame.activationFailures); - } - } @Test void playerStrengthRankReferenceKeepsKyuResultsInKyuBand() throws Exception { @@ -3751,26 +3577,6 @@ private static void setDeclaredField( field.set(target, value); } - private Leelaz reusableTrackingKatago() throws Exception { - Leelaz engine = new Leelaz(""); - engine.isLoaded = true; - engine.started = true; - engine.isKatago = true; - engine.commandLists.addAll( - List.of( - "stop", - "boardsize", - "komi", - "kata-get-rules", - "kata-set-rules", - "clear_board", - "play", - "set_position", - "kata-analyze")); - setLeelazField(engine, "endGetCommandList", true); - createdTrackingEngines.add(engine); - return engine; - } private static void installEmptyBoard() throws Exception { Board board = allocate(Board.class); @@ -3778,49 +3584,7 @@ private static void installEmptyBoard() throws Exception { Lizzie.board = board; } - private static ByteArrayOutputStream installOutput(Leelaz engine) throws Exception { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - setLeelazField(engine, "outputStream", new BufferedOutputStream(output)); - return output; - } - - private static boolean dispatchExclusiveLine(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } - - private static void processCommandResponse(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); - } - private static void closeExclusiveSessionForTest(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("exclusiveGtpSession"); - field.setAccessible(true); - Object session = field.get(engine); - if (session == null) { - return; - } - Method cancelInitial = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpInitialStopTimeout", session.getClass()); - cancelInitial.setAccessible(true); - cancelInitial.invoke(engine, session); - Method cancelRelease = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpReleaseStopTimeout", session.getClass()); - cancelRelease.setAccessible(true); - cancelRelease.invoke(engine, session); - Method close = Leelaz.class.getDeclaredMethod("closeExclusiveGtpSession", session.getClass()); - close.setAccessible(true); - close.invoke(engine, session); - } - - private static void setLeelazField(Leelaz engine, String name, Object value) throws Exception { - Field field = Leelaz.class.getDeclaredField(name); - field.setAccessible(true); - field.set(engine, value); - } private static final class EmptyEngineUiFrame extends LizzieFrame { @Override @@ -4449,6 +4213,7 @@ protected NewGameDialog createNewGameDialog() { return dialog; } + @Override protected void showForegroundEngineLeaseConflict() { conflictCount++; @@ -4466,53 +4231,6 @@ public boolean stopAiPlayingAndPolicy() { } } - private static final class RetainedModeActivationFrame extends LizzieFrame { - private int activations; - private int conflicts; - private int activationFailures; - private String lastAction; - private boolean modeReservationAvailableDuringAction; - - @Override - protected void startNewGameReserved() { - probeModeReservation(); - activations++; - lastAction = "new-game"; - } - - @Override - protected void startAnalyzeGameDialogReserved() { - probeModeReservation(); - activations++; - lastAction = "analyze-game"; - } - - @Override - protected void continueAiPlayingReserved( - boolean isGenmove, boolean continueNow, boolean playerIsB, boolean fromShortCut) { - activations++; - lastAction = "continue"; - isPlayingAgainstLeelaz = true; - } - - private void probeModeReservation() { - Leelaz.EngineModeReservation reservation = Lizzie.leelaz.beginEngineModeReservation(); - modeReservationAvailableDuringAction = reservation != null; - if (reservation != null) { - reservation.close(); - } - } - - @Override - protected void showForegroundEngineModeReservationConflict() { - conflicts++; - } - - @Override - protected void showRetainedEngineModeActivationFailure(Leelaz.TrackingHandoffFailure failure) { - activationFailures++; - } - } private static final class CancelledNewGameDialog extends NewGameDialog { private int visibleCount; diff --git a/src/test/java/featurecat/lizzie/gui/LizzieFrameRestartInteractionGateTest.java b/src/test/java/featurecat/lizzie/gui/LizzieFrameRestartInteractionGateTest.java deleted file mode 100644 index f1ec8841a..000000000 --- a/src/test/java/featurecat/lizzie/gui/LizzieFrameRestartInteractionGateTest.java +++ /dev/null @@ -1,620 +0,0 @@ -package featurecat.lizzie.gui; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.awt.Component; -import java.awt.DefaultKeyboardFocusManager; -import java.awt.EventQueue; -import java.awt.GraphicsEnvironment; -import java.awt.KeyEventDispatcher; -import java.awt.KeyboardFocusManager; -import java.awt.Toolkit; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import javax.swing.AbstractAction; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JMenu; -import javax.swing.JMenuBar; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.KeyStroke; -import javax.swing.SwingUtilities; -import javax.swing.TransferHandler; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; - -class LizzieFrameRestartInteractionGateTest { - - @Test - void gateBlocksAndRestoresDesktopEntryPointState() throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - JFrame frame = new JFrame(); - JPanel board = new JPanel(); - AtomicInteger boardMutations = new AtomicInteger(); - AtomicInteger navigationMutations = new AtomicInteger(); - AtomicInteger menuMutations = new AtomicInteger(); - AtomicInteger fileOpenMutations = new AtomicInteger(); - AtomicInteger dragDropMutations = new AtomicInteger(); - AtomicInteger dialogMutations = new AtomicInteger(); - board.setFocusable(true); - board.addMouseListener( - new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent event) { - boardMutations.incrementAndGet(); - board.requestFocusInWindow(); - } - }); - board - .getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "navigate"); - board - .getActionMap() - .put( - "navigate", - new AbstractAction() { - @Override - public void actionPerformed(java.awt.event.ActionEvent event) { - navigationMutations.incrementAndGet(); - } - }); - board.setTransferHandler( - new TransferHandler() { - @Override - public boolean canImport(TransferSupport support) { - return support.isDataFlavorSupported(DataFlavor.stringFlavor); - } - - @Override - public boolean importData(TransferSupport support) { - dragDropMutations.incrementAndGet(); - return true; - } - }); - TransferHandler dragDropEntry = board.getTransferHandler(); - JMenuBar menuBar = new JMenuBar(); - JMenu menu = new JMenu("actions"); - JMenuItem menuAction = new JMenuItem("menu"); - menuAction.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK)); - menuAction.addActionListener(event -> menuMutations.incrementAndGet()); - JMenuItem fileOpen = new JMenuItem("open"); - fileOpen.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); - fileOpen.addActionListener(event -> fileOpenMutations.incrementAndGet()); - menu.add(menuAction); - menu.add(fileOpen); - menuBar.add(menu); - List settingsDialogs = new ArrayList<>(); - List settingsMutations = new ArrayList<>(); - for (String title : List.of("board-size", "komi", "rules")) { - JDialog dialog = new JDialog(frame, title); - JButton mutation = new JButton("apply"); - mutation.addActionListener(event -> dialogMutations.incrementAndGet()); - dialog.add(mutation); - settingsDialogs.add(dialog); - settingsMutations.add(mutation); - } - try { - SwingUtilities.invokeAndWait( - () -> { - frame.setJMenuBar(menuBar); - frame.add(board); - board.setPreferredSize(new java.awt.Dimension(240, 160)); - frame.pack(); - frame.setLocation(100, 100); - frame.setVisible(true); - int dialogX = 380; - for (JDialog dialog : settingsDialogs) { - dialog.pack(); - dialog.setLocation(dialogX, 100); - dialog.setVisible(true); - dialogX += 120; - } - }); - - LizzieFrame.RestartInteractionGate gate = LizzieFrame.beginRestartInteractionGate(frame); - assertFalse(frame.isEnabled()); - settingsDialogs.forEach(dialog -> assertFalse(dialog.isEnabled())); - assertTrue(boardMutations.get() == 0); - assertTrue(navigationMutations.get() == 0); - assertTrue(menuMutations.get() == 0); - assertTrue(fileOpenMutations.get() == 0); - assertNull(board.getTransferHandler()); - assertTrue(dragDropMutations.get() == 0); - assertTrue(dialogMutations.get() == 0); - - postKeyEvent(board, KeyEvent.VK_RIGHT, 0); - postKeyEvent(board, KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK); - postKeyEvent(board, KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK); - assertTrue(navigationMutations.get() == 0); - assertTrue(menuMutations.get() == 0); - assertTrue(fileOpenMutations.get() == 0); - - gate.close(); - gate.close(); - assertTrue(frame.isEnabled()); - settingsDialogs.forEach(dialog -> assertTrue(dialog.isEnabled())); - assertSame(dragDropEntry, board.getTransferHandler()); - - SwingUtilities.invokeAndWait( - () -> { - board.dispatchEvent( - new MouseEvent( - board, - MouseEvent.MOUSE_CLICKED, - System.currentTimeMillis(), - 0, - board.getWidth() / 2, - board.getHeight() / 2, - 1, - false, - MouseEvent.BUTTON1)); - board.getActionMap().get("navigate").actionPerformed(null); - menuAction.doClick(); - fileOpen.doClick(); - for (JButton mutation : settingsMutations) { - mutation.doClick(); - } - assertTrue( - board - .getTransferHandler() - .importData( - new TransferHandler.TransferSupport( - board, new StringSelection("sgf-file")))); - }); - assertTrue(boardMutations.get() == 1); - assertTrue(navigationMutations.get() == 1); - assertTrue(menuMutations.get() == 1); - assertTrue(fileOpenMutations.get() == 1); - assertTrue(dragDropMutations.get() == 1); - assertTrue(dialogMutations.get() == 3); - } finally { - SwingUtilities.invokeAndWait( - () -> { - for (JDialog dialog : settingsDialogs) { - dialog.dispose(); - } - frame.dispose(); - }); - } - } - - private static void postKeyEvent(Component source, int keyCode, int modifiers) - throws Exception { - EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue(); - queue.postEvent( - new KeyEvent( - source, - KeyEvent.KEY_PRESSED, - System.currentTimeMillis(), - modifiers, - keyCode, - KeyEvent.CHAR_UNDEFINED)); - SwingUtilities.invokeAndWait(() -> {}); - } - - @Test - void gateDisablesExistingOwnedWindowsAndRestoresOriginalStateExactlyOnce() throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - JFrame frame = new JFrame(); - JDialog enabledDialog = new JDialog(frame); - JDialog disabledDialog = new JDialog(frame); - JDialog nestedDialog = new JDialog(enabledDialog); - List mutationsOnEdt = new ArrayList<>(); - try { - SwingUtilities.invokeAndWait( - () -> { - frame.setEnabled(true); - enabledDialog.setEnabled(true); - disabledDialog.setEnabled(false); - nestedDialog.setEnabled(true); - frame.addPropertyChangeListener( - "enabled", event -> mutationsOnEdt.add(SwingUtilities.isEventDispatchThread())); - enabledDialog.addPropertyChangeListener( - "enabled", event -> mutationsOnEdt.add(SwingUtilities.isEventDispatchThread())); - disabledDialog.addPropertyChangeListener( - "enabled", event -> mutationsOnEdt.add(SwingUtilities.isEventDispatchThread())); - nestedDialog.addPropertyChangeListener( - "enabled", event -> mutationsOnEdt.add(SwingUtilities.isEventDispatchThread())); - }); - - LizzieFrame.RestartInteractionGate gate = LizzieFrame.beginRestartInteractionGate(frame); - - assertFalse(frame.isEnabled()); - assertFalse(enabledDialog.isEnabled()); - assertFalse(disabledDialog.isEnabled()); - assertFalse(nestedDialog.isEnabled()); - - gate.close(); - gate.close(); - - assertTrue(frame.isEnabled()); - assertTrue(enabledDialog.isEnabled()); - assertFalse(disabledDialog.isEnabled()); - assertTrue(nestedDialog.isEnabled()); - assertTrue(mutationsOnEdt.stream().allMatch(Boolean::booleanValue)); - } finally { - SwingUtilities.invokeAndWait( - () -> { - nestedDialog.dispose(); - disabledDialog.dispose(); - enabledDialog.dispose(); - frame.dispose(); - }); - } - } - - @Test - void gateRollsBackAlreadyDisabledWindowsWhenAnOwnedWindowFails() throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - JFrame frame = new JFrame(); - FailingDisableDialog dialog = new FailingDisableDialog(frame); - try { - SwingUtilities.invokeAndWait( - () -> { - frame.setEnabled(true); - dialog.setEnabled(true); - }); - - assertThrows( - IllegalStateException.class, () -> LizzieFrame.beginRestartInteractionGate(frame)); - - assertTrue(frame.isEnabled()); - assertTrue(dialog.isEnabled()); - } finally { - SwingUtilities.invokeAndWait( - () -> { - dialog.dispose(); - frame.dispose(); - }); - } - } - - @Test - void gateRollsBackEveryPartialMutationWhenDirectEdtDisableThrowsError() throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - KeyboardFocusManager previousFocusManager = - KeyboardFocusManager.getCurrentKeyboardFocusManager(); - InspectingKeyboardFocusManager focusManager = new InspectingKeyboardFocusManager(); - InspectingKeyboardFocusManager.install(focusManager); - JFrame frame = new JFrame(); - AssertionError disableFailure = new AssertionError("controlled disable error"); - AssertionError restoreFailure = new AssertionError("controlled rollback error"); - ErrorOnDisableDialog dialog = - new ErrorOnDisableDialog(frame, disableFailure, restoreFailure); - try { - SwingUtilities.invokeAndWait( - () -> { - frame.setEnabled(true); - dialog.setEnabled(true); - dialog.arm(); - }); - int dispatcherCount = focusManager.dispatcherCount(); - AtomicReference observed = new AtomicReference<>(); - - SwingUtilities.invokeAndWait( - () -> - observed.set( - assertThrows( - AssertionError.class, - () -> LizzieFrame.beginRestartInteractionGate(frame)))); - - assertSame(disableFailure, observed.get()); - assertEquals(1, disableFailure.getSuppressed().length); - assertSame(restoreFailure, disableFailure.getSuppressed()[0]); - assertTrue(frame.isEnabled()); - assertTrue(dialog.isEnabled()); - assertEquals(dispatcherCount, focusManager.dispatcherCount()); - } finally { - dialog.disarm(); - SwingUtilities.invokeAndWait( - () -> { - dialog.dispose(); - frame.dispose(); - }); - InspectingKeyboardFocusManager.install(previousFocusManager); - } - } - - @Test - void gateCloseRestoresRemainingResourcesAndRemovesDispatcherAfterErrors() throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - KeyboardFocusManager previousFocusManager = - KeyboardFocusManager.getCurrentKeyboardFocusManager(); - InspectingKeyboardFocusManager focusManager = new InspectingKeyboardFocusManager(); - InspectingKeyboardFocusManager.install(focusManager); - JFrame frame = new JFrame(); - AssertionError windowRestoreFailure = new AssertionError("controlled window restore error"); - AssertionError transferRestoreFailure = - new AssertionError("controlled transfer-handler restore error"); - ErrorOnRestoreDialog dialog = new ErrorOnRestoreDialog(frame, windowRestoreFailure); - ErrorOnRestorePanel board = new ErrorOnRestorePanel(transferRestoreFailure); - TransferHandler transferHandler = new TransferHandler("name"); - try { - SwingUtilities.invokeAndWait( - () -> { - frame.add(board); - frame.setEnabled(true); - dialog.setEnabled(true); - board.setTransferHandler(transferHandler); - dialog.arm(); - board.arm(); - }); - int dispatcherCount = focusManager.dispatcherCount(); - LizzieFrame.RestartInteractionGate gate = - LizzieFrame.beginRestartInteractionGate(frame); - assertFalse(frame.isEnabled()); - assertFalse(dialog.isEnabled()); - assertNull(board.getTransferHandler()); - AtomicReference observed = new AtomicReference<>(); - - SwingUtilities.invokeAndWait( - () -> observed.set(assertThrows(AssertionError.class, gate::close))); - - assertSame(windowRestoreFailure, observed.get()); - assertEquals(1, windowRestoreFailure.getSuppressed().length); - assertSame(transferRestoreFailure, windowRestoreFailure.getSuppressed()[0]); - assertTrue(frame.isEnabled()); - assertTrue(dialog.isEnabled()); - assertSame(transferHandler, board.getTransferHandler()); - assertEquals(dispatcherCount, focusManager.dispatcherCount()); - gate.close(); - } finally { - dialog.disarm(); - board.disarm(); - SwingUtilities.invokeAndWait( - () -> { - dialog.dispose(); - frame.dispose(); - }); - InspectingKeyboardFocusManager.install(previousFocusManager); - } - } - - @Test - void interruptedBeginWaitsForItsPostedMutationAndRollsBackTheAbandonedGate() - throws Exception { - Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); - CountingEnabledFrame frame = new CountingEnabledFrame(); - JPanel board = new JPanel(); - TransferHandler transferHandler = new TransferHandler("text"); - CountDownLatch edtBlocked = new CountDownLatch(1); - CountDownLatch releaseEdt = new CountDownLatch(1); - AtomicReference beginFailure = new AtomicReference<>(); - AtomicBoolean interruptPreserved = new AtomicBoolean(); - Thread beginWorker = - new Thread( - () -> { - try { - LizzieFrame.beginRestartInteractionGate(frame); - } catch (Throwable failure) { - beginFailure.set(failure); - interruptPreserved.set(Thread.currentThread().isInterrupted()); - } - }, - "controlled-restart-gate-begin"); - try { - SwingUtilities.invokeAndWait( - () -> { - board.setTransferHandler(transferHandler); - frame.add(board); - frame.setEnabled(true); - frame.armTransitionCounting(); - }); - SwingUtilities.invokeLater( - () -> { - edtBlocked.countDown(); - try { - releaseEdt.await(); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - } - }); - assertTrue(edtBlocked.await(2, TimeUnit.SECONDS)); - - beginWorker.start(); - assertTrue(awaitThreadState(beginWorker, Thread.State.WAITING, 2, TimeUnit.SECONDS)); - beginWorker.interrupt(); - beginWorker.join(150L); - assertTrue( - beginWorker.isAlive(), - "an interrupted caller must retain cleanup ownership until the posted begin completes"); - - releaseEdt.countDown(); - beginWorker.join(TimeUnit.SECONDS.toMillis(2)); - assertFalse(beginWorker.isAlive()); - SwingUtilities.invokeAndWait(() -> {}); - - assertTrue(beginFailure.get() instanceof IllegalStateException); - assertTrue(interruptPreserved.get()); - assertTrue(frame.isEnabled()); - assertSame(transferHandler, board.getTransferHandler()); - assertEquals(1, frame.disableTransitions.get(), "disable transitions"); - assertEquals(1, frame.enableTransitions.get(), "enable transitions"); - } finally { - releaseEdt.countDown(); - beginWorker.interrupt(); - beginWorker.join(TimeUnit.SECONDS.toMillis(2)); - SwingUtilities.invokeAndWait(frame::dispose); - } - } - - private static boolean awaitThreadState( - Thread thread, Thread.State expected, long timeout, TimeUnit unit) - throws InterruptedException { - long deadline = System.nanoTime() + unit.toNanos(timeout); - while (System.nanoTime() < deadline) { - if (thread.getState() == expected) { - return true; - } - Thread.sleep(5L); - } - return thread.getState() == expected; - } - - private static final class CountingEnabledFrame extends JFrame { - private final AtomicInteger disableTransitions = new AtomicInteger(); - private final AtomicInteger enableTransitions = new AtomicInteger(); - private boolean countTransitions; - - private void armTransitionCounting() { - countTransitions = true; - } - - @Override - public void setEnabled(boolean enabled) { - boolean changed = enabled != isEnabled(); - super.setEnabled(enabled); - if (!countTransitions || !changed) { - return; - } - if (enabled) { - enableTransitions.incrementAndGet(); - } else { - disableTransitions.incrementAndGet(); - } - } - } - - private static final class FailingDisableDialog extends JDialog { - private boolean failNextDisable = true; - - private FailingDisableDialog(JFrame owner) { - super(owner); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (!enabled && failNextDisable) { - failNextDisable = false; - throw new IllegalStateException("controlled gate failure"); - } - } - } - - private static final class ErrorOnDisableDialog extends JDialog { - private final AssertionError disableFailure; - private final AssertionError restoreFailure; - private boolean armed; - private boolean failDisable = true; - private boolean failRestore = true; - - private ErrorOnDisableDialog( - JFrame owner, AssertionError disableFailure, AssertionError restoreFailure) { - super(owner); - this.disableFailure = disableFailure; - this.restoreFailure = restoreFailure; - } - - private void arm() { - armed = true; - } - - private void disarm() { - armed = false; - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (armed && !enabled && failDisable) { - failDisable = false; - throw disableFailure; - } - if (armed && enabled && failRestore) { - failRestore = false; - throw restoreFailure; - } - } - } - - private static final class ErrorOnRestoreDialog extends JDialog { - private final AssertionError restoreFailure; - private boolean armed; - private boolean disabledByGate; - - private ErrorOnRestoreDialog(JFrame owner, AssertionError restoreFailure) { - super(owner); - this.restoreFailure = restoreFailure; - } - - private void arm() { - armed = true; - } - - private void disarm() { - armed = false; - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (armed && !enabled) { - disabledByGate = true; - } else if (armed && enabled && disabledByGate) { - disabledByGate = false; - throw restoreFailure; - } - } - } - - private static final class ErrorOnRestorePanel extends JPanel { - private final AssertionError restoreFailure; - private boolean armed; - private boolean clearedByGate; - - private ErrorOnRestorePanel(AssertionError restoreFailure) { - this.restoreFailure = restoreFailure; - } - - private void arm() { - armed = true; - } - - private void disarm() { - armed = false; - } - - @Override - public void setTransferHandler(TransferHandler handler) { - super.setTransferHandler(handler); - if (armed && handler == null) { - clearedByGate = true; - } else if (armed && handler != null && clearedByGate) { - clearedByGate = false; - throw restoreFailure; - } - } - } - - private static final class InspectingKeyboardFocusManager - extends DefaultKeyboardFocusManager { - private static void install(KeyboardFocusManager focusManager) { - setCurrentKeyboardFocusManager(focusManager); - } - - private int dispatcherCount() { - List dispatchers = getKeyEventDispatchers(); - return dispatchers == null ? 0 : dispatchers.size(); - } - } - -} diff --git a/src/test/java/featurecat/lizzie/gui/TrackingProductionCutoverTest.java b/src/test/java/featurecat/lizzie/gui/TrackingProductionCutoverTest.java index 621853347..0478803fa 100644 --- a/src/test/java/featurecat/lizzie/gui/TrackingProductionCutoverTest.java +++ b/src/test/java/featurecat/lizzie/gui/TrackingProductionCutoverTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import featurecat.lizzie.Config; @@ -13,7 +12,6 @@ import featurecat.lizzie.analysis.MoveData; import featurecat.lizzie.analysis.MoveRankEvaluationMode; import featurecat.lizzie.analysis.ReadBoard; -import featurecat.lizzie.analysis.ReadBoardTrackingEligibilityAdapter; import featurecat.lizzie.analysis.TrackingAnalysisController; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardData; @@ -34,489 +32,609 @@ class TrackingProductionCutoverTest { @Test - void localAndStableReadBoardEntriesUseTheSameCurrentEngineController() throws Exception { + void userPausedAnalysisDoesNotStartCapabilityProbe() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame frame = environment.frame; - - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - assertTrue(environment.commands().contains("800000000 stop\n")); - TrackingAnalysisController controller = frame.trackingAnalysisController(); - assertSame(controller, frame.trackingAnalysisController()); - - controller.clear(); - environment.completeInitialFence(800000000); - environment.installStableReadBoard(); - - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("B2")); - assertSame(controller, frame.trackingAnalysisController()); - assertTrue( - controller.snapshot().context().readBoardContext().isPresent(), - "stable ReadBoard entry must bind its accepted frame identity to the same controller"); - assertTrue(environment.commands().contains("800000001 stop\n")); + Field paused = LizzieFrame.class.getDeclaredField("userAnalysisPaused"); + paused.setAccessible(true); + paused.setBoolean(environment.frame, true); + assertFalse(environment.engine.startMoveFocusProbeAfterInitialization()); + assertFalse(environment.engine.ponderIfAnalysisControlAllows()); + assertEquals("", environment.commands()); + assertEquals(Leelaz.MoveFocusCapability.UNKNOWN, environment.engine.moveFocusCapability()); } } @Test - void productionEntryRemovesPendingAndClearsCurrentImmediately() throws Exception { + void userPauseDuringRejectedProbeDoesNotRestartOrdinaryAnalysis() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame frame = environment.frame; - - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("B2")); - - assertTrue(frame.removeTrackingPoint("B2")); - assertEquals(List.of("A1"), List.copyOf(frame.trackingDisplaySnapshot().selectedPoints())); - - frame.clearTrackingPoints(); - - assertTrue(frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); - assertTrue(environment.commands().contains("800000002 stop\n")); + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + String probe = environment.commands().trim(); + String id = probe.substring(0, probe.indexOf(' ')); + environment.engine.pauseForAnalysisControl(() -> {}); + environment.dispatch("?" + id + " unknown analyze option focus"); + environment.dispatch(""); + environment.respondedCommands = 1; + environment.settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.UNSUPPORTED, environment.engine.moveFocusCapability()); + assertFalse(environment.engine.isPondering()); + assertEquals(1, environment.commands().lines().filter(line -> line.contains("kata-analyze")).count()); } } @Test - void removingCurrentTrackingPointResumesPriorNormalAnalysis() throws Exception { + void successfulProbeAfterUserPauseDoesNotRestartOrdinaryAnalysis() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame frame = environment.frame; - environment.engine.ponder(); - environment.processCommandResponse("="); - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - - assertTrue(frame.removeTrackingPoint("A1")); - environment.completeFinalFence(800000002); - - assertTrue(environment.engine.isPondering()); - assertTrue(environment.commands().endsWith("kata-analyze B 10\n"), environment.commands()); - assertTrue(environment.engine.isResponseUpToDate()); - environment.sendOrdinaryInfo( - "info move B2 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv B2"); - assertEquals(1, environment.engine.getBestMoves().size()); + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + String probe = environment.commands().trim(); + String id = probe.substring(0, probe.indexOf(' ')); + environment.dispatch("=" + id); + environment.respondedCommands = 1; + environment.streaming = true; + environment.engine.pauseForAnalysisControl(() -> {}); + environment.settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.SUPPORTED, environment.engine.moveFocusCapability()); + assertFalse(environment.engine.isPondering()); + assertEquals(1, environment.commands().lines().filter(line -> line.contains("kata-analyze")).count()); } } @Test - void clearingTrackingPointsResumesPriorNormalAnalysis() throws Exception { + void probeCompletionRespectsFramePauseEvenWithoutANewPauseGeneration() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame frame = environment.frame; - environment.engine.ponder(); - environment.processCommandResponse("="); - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - - frame.clearTrackingPoints(); - environment.completeFinalFence(800000002); - - assertTrue(environment.engine.isPondering()); - assertTrue(environment.commands().endsWith("kata-analyze B 10\n"), environment.commands()); - assertTrue(environment.engine.isResponseUpToDate()); - environment.sendOrdinaryInfo( - "info move B2 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv B2"); - assertEquals(1, environment.engine.getBestMoves().size()); + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + Field paused = LizzieFrame.class.getDeclaredField("userAnalysisPaused"); + paused.setAccessible(true); + paused.setBoolean(environment.frame, true); + environment.settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.SUPPORTED, environment.engine.moveFocusCapability()); + assertFalse(environment.engine.isPondering()); + assertEquals(1, environment.commands().lines().filter(line -> line.contains("kata-analyze")).count()); } } @Test - void playingMoveDuringTrackingResumesNormalAnalysisThroughProductionController() - throws Exception { - assertPlayingMoveDuringTrackingAcceptsNormalAnalysisAfterResponse("=", true); + void capabilityProbeCannotGrantSupportBeforeItsFinalResponseTerminates() throws Exception { + try (TestEnvironment environment = TestEnvironment.open()) { + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (!environment.commands().contains("focus") && System.nanoTime() < deadline) { + Thread.sleep(1); + } + assertEquals(Leelaz.MoveFocusCapability.PROBING, environment.engine.moveFocusCapability()); + String[] commands = environment.commands().trim().split("\n"); + String probe = commands[0]; + assertTrue(probe.contains("focus pass 0"), probe); + String probeId = probe.substring(0, probe.indexOf(' ')); + environment.dispatch("=" + probeId); + assertEquals(Leelaz.MoveFocusCapability.PROBING, environment.engine.moveFocusCapability()); + assertEquals(TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, + environment.frame.addTrackingPoint("A1")); + environment.respondedCommands = 1; + environment.streaming = true; + environment.settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.SUPPORTED, environment.engine.moveFocusCapability()); + } } @Test - void failedPlayDuringTrackingDoesNotAcceptNormalAnalysisForTheUnplayedPosition() - throws Exception { - assertPlayingMoveDuringTrackingAcceptsNormalAnalysisAfterResponse("? illegal move", false); - } - - private void assertPlayingMoveDuringTrackingAcceptsNormalAnalysisAfterResponse( - String playResponse, boolean expectNormalAnalysis) throws Exception { - BoardRenderer previousBoardRenderer = LizzieFrame.boardRenderer; + void rejectedProbePreservesOrdinaryAnalysisAndIsNotRepeated() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame.boardRenderer = new BoardRenderer(false); - Lizzie.config.analyzeBlack = true; - Lizzie.config.analyzeWhite = true; - environment.engine.ponder(); - environment.processCommandResponse("="); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + String probe = environment.commands().trim(); + String id = probe.substring(0, probe.indexOf(' ')); + environment.dispatch("?" + id + " unknown analyze option focus"); + assertEquals(Leelaz.MoveFocusCapability.PROBING, environment.engine.moveFocusCapability()); + environment.dispatch(""); + environment.respondedCommands = 1; + environment.settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.UNSUPPORTED, environment.engine.moveFocusCapability()); + assertEquals(TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - - Lizzie.board.place(0, 0, Stone.BLACK); - environment.completeFinalFence(800000002); - String beforeAck = environment.commands().trim(); - String playCommand = beforeAck.substring(beforeAck.lastIndexOf('\n') + 1); - assertTrue(playCommand.endsWith("play B A2"), beforeAck); - String playId = playCommand.substring(0, playCommand.indexOf(' ')); - environment.processCommandResponse( - playResponse.substring(0, 1) + playId + playResponse.substring(1)); - environment.sendOrdinaryInfo( - "info move B2 visits 40 winrate 0.51 scoreLead 2.5 prior 0.2 pv B2"); - - String commands = environment.commands(); - assertTrue(environment.engine.isPondering()); - assertEquals( - expectNormalAnalysis, - commands.lastIndexOf("kata-analyze") > commands.lastIndexOf("play B A2"), - commands); - assertEquals(expectNormalAnalysis ? 1 : 0, environment.engine.getBestMoves().size()); - } finally { - LizzieFrame.boardRenderer = previousBoardRenderer; + environment.sendOrdinaryInfo("info move A1 visits 40 order 0 winrate 0.51 pv A1"); + assertEquals(40, candidate("A1").playouts); + environment.engine.ponder(); + environment.settleCommands(); + assertEquals(1, environment.commands().lines().filter(line -> line.contains("focus pass")).count()); } } @Test - void rendererGateSuppressesStaleNodeAndPonderInvalidationDoesNotReacquire() throws Exception { + void retiredProbeResponseCannotGrantSupportToTheReplacementBinding() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - LizzieFrame frame = environment.frame; - environment.engine.Pondering(); - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - TrackingAnalysisController.DisplaySnapshot original = frame.trackingDisplaySnapshot(); - assertTrue(frame.isTrackingDisplayCurrent(original)); - - Lizzie.board.setHistory(new BoardHistoryList(BoardData.empty(2, 2))); - - assertFalse(frame.isTrackingDisplayCurrent(original)); - frame.onMainEnginePonder(); - - assertTrue(frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); - assertTrue(environment.commands().contains("800000002 stop\n")); - assertFalse(environment.commands().contains("800000003 stop\n")); - environment.completeFinalFence(800000002); - assertFalse(environment.engine.isPondering()); + assertTrue(environment.engine.startMoveFocusProbeAfterInitialization()); + String probe = environment.commands().trim(); + String oldId = probe.substring(0, probe.indexOf(' ')); + Method install = Leelaz.class.getDeclaredMethod("installFreshCommandStreamsForTest", + java.io.InputStream.class, java.io.OutputStream.class, java.io.InputStream.class); + install.setAccessible(true); + install.invoke(environment.engine, java.io.InputStream.nullInputStream(), environment.output, + java.io.InputStream.nullInputStream()); + environment.output.reset(); + environment.respondedCommands = 0; + environment.streaming = false; + environment.dispatch("=" + oldId); + environment.dispatch(""); + assertEquals(Leelaz.MoveFocusCapability.UNKNOWN, environment.engine.moveFocusCapability()); + environment.startSupportedAnalysis(); + assertEquals(1, environment.commands().lines().filter(line -> line.contains("focus pass")).count()); } } @Test - void internalTrackingInvalidationDoesNotRestorePriorNormalAnalysis() throws Exception { + void localAndStableReadBoardEntriesUseOrdinaryFocusAndKeepAttentionAfterTarget() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { + environment.startSupportedAnalysis(); LizzieFrame frame = environment.frame; - environment.engine.ponder(); - environment.processCommandResponse("="); assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - - frame.invalidateTrackingAnalysis(); - environment.completeFinalFence(800000002); + environment.settleCommands(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("B2")); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(50, 110, 120, 0.50, 0.60)); + assertTrue(frame.trackingDisplaySnapshot().activePoints().isEmpty(), + "active=" + frame.trackingDisplaySnapshot().activePoints() + " visits=" + + frame.trackingDisplaySnapshot().visits() + " root=" + Lizzie.board.getData().rootVisits + + " commands=" + environment.commands()); + assertEquals(java.util.Set.of("A1", "B2"), frame.trackingDisplaySnapshot().selectedPoints()); + environment.settleCommands(); + assertFalse(frame.trackingDisplaySnapshot().active()); + assertTrue(environment.engine.isPondering()); + assertFalse(environment.commands().contains("allow B")); + frame.clearTrackingPoints(); + environment.settleCommands(); + environment.installStableReadBoard(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); + assertTrue(frame.trackingDisplaySnapshot().context().readBoardContext().isPresent()); + } + } - assertFalse(environment.engine.isPondering()); - assertTrue(frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); - assertFalse(environment.commands().endsWith("kata-analyze B 10\n")); + @Test + void returningToAcceptedReadBoardPositionAllowsNewEvaluationWithoutAnotherFrame() throws Exception { + BoardRenderer previousRenderer = LizzieFrame.boardRenderer; + try (TestEnvironment environment = TestEnvironment.open()) { + LizzieFrame.boardRenderer = new BoardRenderer(false); + environment.startSupportedAnalysis(); + Lizzie.board.place(0, 0, Stone.BLACK); + environment.settleCommands(); + environment.installStableReadBoard("re=3,0", "re=0,0"); + ReadBoard helper = environment.frame.readBoard; + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + environment.sendOrdinaryInfo("info move A1 visits 20 order 0 winrate 0.51 pv A1 rootInfo visits 30"); + assertEquals(20, environment.frame.trackingDisplaySnapshot().visits().get("A1")); + long acceptedRevision = Lizzie.board.getContextRevision(); + assertTrue(Lizzie.board.previousMove(false)); + environment.settleCommands(); + assertFalse(helper.snapshot().stable()); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); + assertTrue(Lizzie.board.nextMove(false)); + assertFalse(environment.frame.canStartTrackingAnalysis()); + environment.settleCommands(); + assertTrue(Lizzie.board.getContextRevision() > acceptedRevision); + assertTrue(helper.snapshot().stable()); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("B2")); + } finally { + LizzieFrame.boardRenderer = previousRenderer; } } @Test - void trackingDisplayChangesRequestRefreshWithoutOrdinaryParserRepaint() throws Exception { + void nativeCrlfIsAcceptedButMalformedCellsCannotPublishAnAcceptedFrame() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - TrackingFrame frame = (TrackingFrame) environment.frame; - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - int beforeInfo = frame.analysisRefreshRequests; + environment.installStableReadBoard(); + ReadBoard readBoard = environment.frame.readBoard; + readBoard.parseLine("re=0,0\r\n"); + assertFalse(readBoard.snapshot().stable()); + readBoard.parseLine("re=0,0\r\n"); + readBoard.parseLine("end\r\n"); + assertTrue(readBoard.snapshot().stable()); + readBoard.parseLine("re=0junk,0"); + readBoard.parseLine("re=0,0"); + readBoard.parseLine("end"); + assertFalse(readBoard.snapshot().stable()); + assertEquals( + TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, + environment.frame.addTrackingPoint("A1")); + } + } - environment.sendTrackingInfo("info move A1 visits 100 winrate 0.51 scoreLead 1.5 pv A1"); + @Test + void stoppingDiscardsMalformedSamplingBeforeTheFirstResumedFrame() throws Exception { + for (String boundary : List.of("endsync", "stopsync")) { + try (TestEnvironment environment = TestEnvironment.open()) { + environment.installStableReadBoard(); + ReadBoard helper = environment.frame.readBoard; + helper.parseLine("re=x,0"); + helper.parseLine(boundary); + assertFalse(environment.frame.canStartTrackingAnalysis()); + helper.parseLine("re=0,0"); + helper.parseLine("re=0,0"); + helper.parseLine("end"); + environment.completeSyncConfirmation(); + assertTrue(environment.frame.canStartTrackingAnalysis(), boundary); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + } + } + } - assertTrue(frame.analysisRefreshRequests > beforeInfo); - int beforeCompletion = frame.analysisRefreshRequests; - environment.completeFinalFence(800000002); - assertTrue(frame.analysisRefreshRequests > beforeCompletion); + @Test + void acceptedNodeIdentityDoesNotOverridePositionHistoryOrHelperMismatch() throws Exception { + List mutations = + List.of( + () -> Lizzie.board.getHistory().getData().stones[0] = Stone.BLACK, + () -> Lizzie.board.getHistory().getData().blackToPlay = false, + () -> Board.boardWidth = 3, + () -> Board.boardHeight = 3, + () -> Lizzie.config.currentKataGoRules = "japanese", + () -> Lizzie.board.getHistory().getGameInfo().setKomi(0.5), + () -> + Lizzie.board.setHistory(new BoardHistoryList(Lizzie.board.getHistory().getData())), + () -> Lizzie.frame.readBoard = null); + for (Runnable mutation : mutations) { + try (TestEnvironment environment = TestEnvironment.open()) { + environment.installStableReadBoard(); + ReadBoard helper = environment.frame.readBoard; + mutation.run(); + assertFalse(helper.snapshot().stable()); + if (environment.frame.readBoard != null) { + assertFalse(environment.frame.canStartTrackingAnalysis()); + } + } } } @Test - void trackingOverlayIsIndependentOfCandidateVisibilityAndSuppressedForBranchOrStaleNode() - throws Exception { - try (TestEnvironment environment = TestEnvironment.open()) { - TrackingFrame frame = (TrackingFrame) environment.frame; - Font previousWinrateFont = LizzieFrame.winrateFont; - Font previousPlayoutsFont = LizzieFrame.playoutsFont; - LizzieFrame.winrateFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - LizzieFrame.playoutsFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - try { - assertEquals(TrackingAnalysisController.AddResult.ADDED, frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.51 scoreLead 1.5 pv A1"); - Lizzie.config.showBestMoves = false; - frame.isShowingHeatmap = true; - frame.isShowingPolicy = true; - BoardRenderer renderer = configuredRenderer(); - - assertTrue(hasVisiblePaint(renderTrackingOverlay(renderer))); - - setField(renderer, BoardRenderer.class, "isShowingBranch", true); - assertFalse(hasVisiblePaint(renderTrackingOverlay(renderer))); - setField(renderer, BoardRenderer.class, "isShowingBranch", false); - Lizzie.board.setHistory(new BoardHistoryList(BoardData.empty(2, 2))); - assertFalse(hasVisiblePaint(renderTrackingOverlay(renderer))); - } finally { - LizzieFrame.winrateFont = previousWinrateFont; - LizzieFrame.playoutsFont = previousPlayoutsFont; + void stoppedOrIncompleteFrameCannotReadmitAnOldAcceptedPosition() throws Exception { + for (String boundary : List.of("endsync", "stopsync", "clear", "re=0,0")) { + try (TestEnvironment environment = TestEnvironment.open()) { + environment.installStableReadBoard(); + ReadBoard helper = environment.frame.readBoard; + helper.parseLine(boundary); + assertFalse(environment.frame.canStartTrackingAnalysis()); + helper.onLocalHistoryNavigation(); + assertFalse(helper.snapshot().stable()); + assertEquals( + TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, + environment.frame.addTrackingPoint("A1")); } } } @Test - void trackingResultUsesFixedInteriorAndLiveQualityOutline() throws Exception { + void lateFrameAcceptanceCannotReopenAStoppedHelper() throws Exception { + BoardRenderer previousRenderer = LizzieFrame.boardRenderer; try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.trackingPointInteriorColor = new Color(10, 20, 30); - Lizzie.config.trackingPointInteriorOpacityPercent = 50; - Lizzie.config.showTrackingPointOutline = true; - Lizzie.config.trackingPointOutlineOpacityPercent = 100; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - environment.installOrdinaryBestMove("B2", 1000, 60.0, 5.0); + LizzieFrame.boardRenderer = new BoardRenderer(false); + environment.installStableReadBoard(); + ReadBoard helper = environment.frame.readBoard; + java.util.concurrent.atomic.AtomicBoolean reached = + new java.util.concurrent.atomic.AtomicBoolean(); + ((TrackingBoard) Lizzie.board).afterSyncMove = + () -> { + assertFalse(environment.frame.canStartTrackingAnalysis()); + helper.parseLine("endsync"); + reached.set(true); + }; + helper.parseLine("re=3,0"); + helper.parseLine("re=0,0"); + helper.parseLine("end"); + environment.acknowledgePositionCommands(); + assertTrue(reached.get()); + assertFalse(helper.snapshot().stable()); assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage rendered = renderTrackingOverlay(configuredRenderer()); - - assertTrue( - countArgb(rendered, new Color(10, 20, 30, 128)) > 200, - "the tracking result disc should use the configured fixed interior"); - assertTrue( - countArgb(rendered, new Color(200, 140, 50)) > 20, - "a 10-point winrate and 3-point score loss should color the dashed outline"); + TrackingAnalysisController.AddResult.LEASE_UNAVAILABLE, + environment.frame.addTrackingPoint("B2")); + } finally { + LizzieFrame.boardRenderer = previousRenderer; } } @Test - void disablingTrackingOutlineKeepsTheResultInteriorWithoutQualityColor() throws Exception { + void sameReadBoardFrameRetainsFocusButPendingFrameRejectsNewRequest() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.trackingPointInteriorColor = new Color(10, 20, 30); - Lizzie.config.trackingPointInteriorOpacityPercent = 100; - Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.trackingPointOutlineOpacityPercent = 100; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - environment.installOrdinaryBestMove("B2", 1000, 60.0, 5.0); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage rendered = renderTrackingOverlay(configuredRenderer()); - - assertTrue(countArgb(rendered, new Color(10, 20, 30)) > 200); - assertEquals(0, countArgb(rendered, new Color(200, 140, 50))); + environment.startSupportedAnalysis(); + environment.installStableReadBoard(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + String before = environment.commands(); + ReadBoard helper = environment.frame.readBoard; + helper.parseLine("re=0,0"); + assertFalse(environment.frame.canStartTrackingAnalysis()); + assertEquals(java.util.Set.of("A1"), environment.frame.trackingDisplaySnapshot().selectedPoints()); + helper.parseLine("re=0,0"); + helper.parseLine("end"); + environment.completeSyncConfirmation(); + assertTrue(environment.frame.canStartTrackingAnalysis()); + assertEquals(before, environment.commands()); + assertEquals(java.util.Set.of("A1"), environment.frame.trackingDisplaySnapshot().activePoints()); } } @Test - void trackingResultUsesNeutralGrayUntilAnOrdinaryBaselineExists() throws Exception { + void explicitFocusAdoptsWholeLowRootPayloadAndCancellationDoesNotRollItBack() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = true; - Lizzie.config.trackingPointOutlineOpacityPercent = 100; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage rendered = renderTrackingOverlay(configuredRenderer()); - - assertTrue( - countArgb(rendered, new Color(112, 118, 124)) > 20, - "a tracking result without an ordinary best candidate should keep a neutral outline"); + environment.startSupportedAnalysis(); + Lizzie.config.enableLizzieCache = true; + environment.sendOrdinaryInfo(payload(10000, 9000, 1000, 0.90, 0.91)); + environment.engine.sendCommand("kata-analyze B 10 rootInfo true"); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(1000, 20, 30, 0.40, 0.60)); + assertEquals(10000, Lizzie.board.getData().rootVisits); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + assertEquals(1000, Lizzie.board.getData().rootVisits); + assertEquals(20, candidate("A1").playouts); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(1000, 100, 40, 0.50, 0.65)); + environment.settleCommands(); + environment.frame.clearTrackingPoints(); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(1000, 100, 40, 0.55, 0.70)); + assertEquals(1000, Lizzie.board.getData().rootVisits); + assertEquals(55.0, candidate("A1").winrate, 0.001); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); + String saved = featurecat.lizzie.rules.SGFParser.saveToString(false); + assertTrue(saved.contains("rootVisits=1000")); + var loaded = featurecat.lizzie.rules.SGFParser.parseSgf(saved, true); + assertEquals(1000, loaded.getData().rootVisits); + assertEquals(100, loaded.getData().bestMoves.stream() + .filter(move -> move.coordinate.equals("A1")).findFirst().orElseThrow().playouts); } } @Test - void trackingResultRecolorsWhenTheOrdinaryBaselineChanges() throws Exception { + void outlineSwitchDoesNotHideCompletedCandidateOrRestartFocus() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { + environment.startSupportedAnalysis(); + environment.sendOrdinaryInfo(payload(200, 120, 130, 0.50, 0.60) + .replace("edgeVisits 0", "edgeVisits 10")); + Lizzie.config.showTrackingPointOutline = false; + BufferedImage ordinary = renderMainBoard(); + String before = environment.commands(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + assertEquals(before, environment.commands()); + assertTrue(environment.frame.trackingDisplaySnapshot().activePoints().isEmpty()); + BufferedImage attended = renderMainBoard(); + assertTrue(java.util.Arrays.equals( + ordinary.getRGB(0, 0, 180, 180, null, 0, 180), + attended.getRGB(0, 0, 180, 180, null, 0, 180))); Lizzie.config.showTrackingPointOutline = true; - Lizzie.config.trackingPointOutlineOpacityPercent = 100; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - environment.installOrdinaryBestMove("B2", 1000, 60.0, 5.0); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.50 scoreLead 2.0 pv A1"); - assertTrue( - countArgb(renderTrackingOverlay(configuredRenderer()), new Color(200, 140, 50)) > 20); - - environment.installOrdinaryBestMove("B2", 1000, 50.0, 2.0); - - assertTrue( - countArgb(renderTrackingOverlay(configuredRenderer()), new Color(0, 180, 0)) > 20, - "the existing tracking outline should be recolored from the current ordinary baseline"); + assertTrue(hasVisiblePaint(renderTrackingOverlay(configuredRenderer()))); + Lizzie.config.showTrackingPointOutline = false; + assertFalse(hasVisiblePaint(renderTrackingOverlay(configuredRenderer()))); + assertEquals(before, environment.commands()); + assertTrue(environment.frame.isTrackingPoint("A1")); } } @Test - void trackingResultOverridesOrdinaryCandidateAtTheSameCoordinate() throws Exception { + void unsupportedEngineModesRejectFocusWithoutWriting() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.trackingPointInteriorColor = new Color(33, 44, 55); - Lizzie.config.trackingPointInteriorOpacityPercent = 100; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - environment.installOrdinaryBestMove("A1", 1000, 60.0, 5.0); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 10 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage rendered = renderMainBoard(); - - assertTrue( - countOpaqueRgb(rendered, new Color(33, 44, 55)) > 200, - "the fixed tracking interior should replace the ordinary candidate at that coordinate"); + environment.startSupportedAnalysis(); + String before = environment.commands(); + environment.engine.useJavaSSH = true; + assertFalse(environment.frame.canStartTrackingAnalysis()); + environment.engine.useJavaSSH = false; + Lizzie.config.extraMode = ExtraMode.Double_Engine; + assertFalse(environment.frame.canStartTrackingAnalysis()); + assertEquals(before, environment.commands()); } } - @Test - void selectedTrackingPointSuppressesTheOrdinaryCandidateWhenOutlineIsDisabled() - throws Exception { + void removingSelectedFocusBeforeItsPhysicalWriteCannotSendTheRetiredPoint() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - Lizzie.config.showBlueRing = true; - environment.installOrdinaryBestMove("A1", 1000, 60.0, 5.0); - - assertTrue(countOpaqueRgb(renderMainBoard(), Color.BLUE) > 10); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - - assertEquals( - 0, - countOpaqueRgb(renderMainBoard(), Color.BLUE), - "the selected tracking point should own the coordinate before its first result arrives"); + environment.startSupportedAnalysis(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + Field field = EngineManager.class.getDeclaredField("ENGINE_GAME_ANALYSIS_OUTPUT_MUTATION_LOCK"); + field.setAccessible(true); + var admission = (java.util.concurrent.locks.ReentrantLock) field.get(null); + var failure = new java.util.concurrent.atomic.AtomicReference(); + Thread writer = new Thread(() -> { + try { environment.settleCommands(); } + catch (Throwable thrown) { failure.set(thrown); } + }, "focus-selected-before-write"); + admission.lock(); + try { + writer.start(); + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (!admission.hasQueuedThread(writer) && System.nanoTime() < deadline) Thread.sleep(1); + assertTrue(admission.hasQueuedThread(writer)); + assertTrue(environment.frame.removeTrackingPoint("A1")); + } finally { + admission.unlock(); + writer.join(3000); + } + assertFalse(writer.isAlive()); + assertEquals(null, failure.get()); + environment.settleCommands(); + assertFalse(environment.commands().contains("focus A1"), environment.commands()); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); + assertTrue(environment.engine.isLoaded()); } } @Test - void selectedTrackingPointUsesTheNeutralPendingOutline() throws Exception { + void addingFocusDoesNotRestartTheOrdinaryTimeBudget() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = true; - Lizzie.config.trackingPointOutlineOpacityPercent = 100; - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - - BufferedImage rendered = renderMainBoard(); - - assertTrue( - countOpaqueRgb(rendered, new Color(112, 118, 124)) > 20, - "a selected point without a result should use the neutral dashed outline"); + environment.startSupportedAnalysis(); + Lizzie.config.limitTime = true; + Lizzie.config.maxAnalyzeTimeMillis = 100; + Thread.sleep(150); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + environment.dispatch(payload(200, 20, 130, 0.50, 0.60)); + environment.settleCommands(); + assertTrue(environment.engine.isStopPonderingByLimit()); + assertFalse(environment.frame.trackingDisplaySnapshot().active()); } } @Test - void transparentDarkInteriorUsesBlackTextAutomaticallyAndAllowsManualOverride() throws Exception { + void manualPauseRetainsAttentionAndAnalysisWithoutRestartingGain() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.trackingPointInteriorColor = Color.BLACK; - Lizzie.config.trackingPointInteriorOpacityPercent = 10; - Lizzie.config.showWinrateInSuggestion = true; - Lizzie.config.showPlayoutsInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - Lizzie.config.trackingPointTextAutoColor = false; - Lizzie.config.trackingPointTextColor = Color.WHITE; - environment.installOrdinaryBestMove("B2", 1000, 60.0, 5.0); - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 100 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage manualWhite = renderMainBoard(); - Lizzie.config.trackingPointTextAutoColor = true; - BufferedImage automatic = renderMainBoard(); - - assertTrue( - opaqueRgbMaskDifferences(manualWhite, automatic, Color.BLACK, 51, 129, 24).size() > 5, - "a mostly transparent dark fill over the light board should automatically use black text"); + environment.startSupportedAnalysis(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(200, 40, 130, 0.50, 0.60)); + environment.engine.pauseForAnalysisControl(() -> {}); + environment.settleCommands(); + assertFalse(environment.engine.isPondering()); + assertEquals(java.util.Set.of("A1"), environment.frame.trackingDisplaySnapshot().selectedPoints()); + assertTrue(environment.frame.trackingDisplaySnapshot().activePoints().isEmpty()); + assertFalse(environment.frame.trackingDisplaySnapshot().cancellationPending()); + assertEquals(40, candidate("A1").playouts); + String paused = environment.commands(); + environment.dispatch(payload(300, 1000, 140, 0.99, 0.60)); + environment.settleCommands(); + assertEquals(paused, environment.commands()); + assertTrue(environment.frame.trackingDisplaySnapshot().activePoints().isEmpty()); + environment.engine.ponder(); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(400, 80, 200, 0.55, 0.65)); + assertEquals(400, Lizzie.board.getData().rootVisits); + assertEquals(80, candidate("A1").playouts); + assertTrue(environment.frame.trackingDisplaySnapshot().activePoints().isEmpty()); } } @Test - void trackingResultReusesOrdinaryCandidateTextLayout() throws Exception { + void clearingTheEngineTreeRetiresExplicitAdoptionEvenAtTheSameDisplayNode() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.useDefaultInfoRowOrder = false; - Lizzie.config.suggestionInfoPlayouts = 1; - Lizzie.config.suggestionInfoScoreLead = 2; - Lizzie.config.suggestionInfoWinrate = 3; - Lizzie.config.showSuggestionOrder = false; - Lizzie.config.showScoreAsDiff = true; - environment.installOrdinaryMoves("B2", 1000000, 60.0, 5.0, "A1", 12345, 50.0, 2.0); - - BufferedImage ordinaryCandidate = renderMainBoard(); - - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 12345 winrate 0.50 scoreLead 2.0 pv A1"); - - BufferedImage trackingResult = renderMainBoard(); + environment.startSupportedAnalysis(); + Lizzie.config.enableLizzieCache = true; + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(1000, 120, 130, 0.50, 0.60)); + environment.settleCommands(); + environment.engine.sendCommand("clear_board"); + environment.settleCommands(); + environment.engine.sendCommand("kata-analyze B 10 rootInfo true"); + environment.settleCommands(); + environment.dispatch(payload(100, 50000, 80, 0.99, 0.60)); + assertEquals(1000, Lizzie.board.getData().rootVisits); + assertEquals(120, candidate("A1").playouts); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); + } + } - List differences = - opaqueRgbMaskDifferences(ordinaryCandidate, trackingResult, Color.BLACK, 51, 129, 30); - assertTrue( - differences.size() <= 2, - "tracking text should reuse ordinary row order, positions, and score-difference baseline; " - + "only antialiasing edge pixels may differ over the translucent interior: " - + differences); + @Test + void focusedPayloadWithoutRootCannotOverwriteOrCompleteOrdinaryAnalysis() throws Exception { + try (TestEnvironment environment = TestEnvironment.open()) { + environment.startSupportedAnalysis(); + environment.sendOrdinaryInfo(payload(200, 20, 30, 0.50, 0.60)); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + environment.dispatch("info move A1 visits 50000 order 0 winrate 0.99 pv A1"); + environment.settleCommands(); + assertEquals(200, Lizzie.board.getData().rootVisits); + assertEquals(20, candidate("A1").playouts); + assertFalse(environment.frame.trackingDisplaySnapshot().active()); } } @Test - void trackingResultUsesTheOrdinaryCandidateInformationVisibility() throws Exception { + void completedEdgeZeroAttentionSurvivesOutlineOffAndRemovalRestoresFiltering() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { + environment.startSupportedAnalysis(); + Lizzie.config.limitMaxSuggestion = 1; Lizzie.config.showTrackingPointOutline = false; - Lizzie.config.showWinrateInSuggestion = false; - Lizzie.config.showScoremeanInSuggestion = false; - Lizzie.config.showSuggestionOrder = false; - environment.installOrdinaryMoves("B2", 1000000, 60.0, 5.0, "A1", 12345, 50.0, 2.0); - - BufferedImage ordinaryCandidate = renderMainBoard(); - - assertEquals( - TrackingAnalysisController.AddResult.ADDED, environment.frame.addTrackingPoint("A1")); - environment.completeInitialFence(800000000); - environment.sendTrackingInfo("info move A1 visits 12345 winrate 0.50 scoreLead 2.0 pv A1"); + environment.sendOrdinaryInfo(payload(200, 120, 130, 0.50, 0.60)); + assertFalse(Lizzie.board.getData().bestMoves.stream().anyMatch(move -> move.coordinate.equals("A1"))); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + assertEquals(0, candidate("A1").edgeVisits); + assertEquals(10, candidate("A1").order); + assertTrue(environment.frame.trackingDisplaySnapshot().activePoints().isEmpty()); + assertTrue(environment.frame.removeTrackingPoint("A1")); + environment.settleCommands(); + assertFalse(Lizzie.board.getData().bestMoves.stream().anyMatch(move -> move.coordinate.equals("A1"))); + assertTrue(Lizzie.board.getData().bestMovesOutOfRange.stream() + .anyMatch(move -> move.coordinate.equals("A1") && move.playouts == 120)); + } + } - BufferedImage trackingResult = renderMainBoard(); - List differences = - opaqueRgbMaskDifferences(ordinaryCandidate, trackingResult, Color.BLACK, 51, 129, 30); - assertTrue( - differences.size() <= 2, - "tracking text should honor the ordinary candidate information switches; only " - + "antialiasing edge pixels may differ over the translucent interior: " - + differences); + @Test + void cancellingBeforeFirstFocusPayloadPreservesTheOldNodeAnalysis() throws Exception { + try (TestEnvironment environment = TestEnvironment.open()) { + environment.startSupportedAnalysis(); + Lizzie.config.enableLizzieCache = true; + environment.sendOrdinaryInfo(payload(10000, 9000, 1000, 0.80, 0.90)); + environment.engine.sendCommand("kata-analyze B 10 rootInfo true"); + environment.settleCommands(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.frame.clearTrackingPoints(); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(1000, 50000, 40, 0.40, 0.60)); + assertEquals(10000, Lizzie.board.getData().rootVisits); + assertEquals(9000, candidate("A1").playouts); + assertEquals(80.0, candidate("A1").winrate, 0.001); + assertTrue(environment.frame.trackingDisplaySnapshot().selectedPoints().isEmpty()); } } @Test - void unsupportedEngineModesAndMissingCapabilitiesAreHiddenBeforeLeaseAcquisition() - throws Exception { + void onlyEachPointsOwnNewHighWaterRenewsItsNoProgressDeadline() throws Exception { try (TestEnvironment environment = TestEnvironment.open()) { - environment.engine.useJavaSSH = true; - assertFalse(environment.frame.canStartTrackingAnalysis()); - - environment.engine.useJavaSSH = false; - Lizzie.config.extraMode = ExtraMode.Double_Engine; - assertFalse(environment.frame.canStartTrackingAnalysis()); + environment.startSupportedAnalysis(); + var deadlines = new java.util.ArrayList(); + Class schedulerType = Class.forName( + "featurecat.lizzie.analysis.TrackingAnalysisController$TimeoutScheduler"); + Class cancellableType = Class.forName( + "featurecat.lizzie.analysis.TrackingAnalysisController$Cancellable"); + Object scheduler = java.lang.reflect.Proxy.newProxyInstance( + schedulerType.getClassLoader(), new Class[] {schedulerType}, (proxy, method, args) -> { + deadlines.add((Runnable) args[1]); + return java.lang.reflect.Proxy.newProxyInstance(cancellableType.getClassLoader(), + new Class[] {cancellableType}, (p, m, a) -> null); + }); + var constructor = TrackingAnalysisController.class.getDeclaredConstructor(schedulerType); + constructor.setAccessible(true); + TrackingAnalysisController controller = + (TrackingAnalysisController) constructor.newInstance(scheduler); + setField(environment.frame, LizzieFrame.class, "trackingAnalysisController", controller); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("A1")); + environment.settleCommands(); + assertEquals(TrackingAnalysisController.AddResult.ADDED, + environment.frame.addTrackingPoint("B2")); + environment.settleCommands(); + environment.sendOrdinaryInfo(payload(40, 20, 30, 0.5, 0.6)); + Runnable a1Deadline = deadlines.get(deadlines.size() - 1); + environment.sendOrdinaryInfo(payload(50, 10, 40, 0.5, 0.6)); + environment.sendOrdinaryInfo(payload(60, 15, 50, 0.5, 0.6)); + a1Deadline.run(); + assertEquals(java.util.Set.of("B2"), controller.snapshot().activePoints()); + assertEquals(java.util.Set.of("A1", "B2"), controller.snapshot().selectedPoints()); + assertTrue(controller.snapshot().visits().get("A1") < controller.snapshot().targetVisits()); + environment.settleCommands(); + assertEquals(java.util.Set.of("B2"), controller.snapshot().activePoints()); + } + } - Lizzie.config.extraMode = ExtraMode.Normal; - environment.engine.commandLists.remove("kata-analyze"); - assertFalse(environment.frame.canStartTrackingAnalysis()); + private static String payload(int root, int a1, int b2, double a1Winrate, double b2Winrate) { + return "info move B2 visits " + b2 + " edgeVisits 30 order 0 winrate " + b2Winrate + + " scoreLead 5 pv B2 info move A1 visits " + a1 + + " edgeVisits 0 order 10 winrate " + a1Winrate + + " scoreLead 2 pv A1 rootInfo visits " + root; + } - environment.engine.commandLists.add("kata-analyze"); - setField(environment.engine, Leelaz.class, "outputStream", null); - assertFalse(environment.frame.canStartTrackingAnalysis()); - } + private static MoveData candidate(String coordinate) { + return Lizzie.board.getData().bestMoves.stream() + .filter(move -> move.coordinate.equals(coordinate)).findFirst().orElseThrow(); } private static BoardRenderer configuredRenderer() throws Exception { @@ -588,99 +706,18 @@ private static BufferedImage renderMainBoard() { } } - private static int countOpaqueRgb(BufferedImage image, Color color) { - int expected = color.getRGB() & 0x00FFFFFF; - int count = 0; - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - int pixel = image.getRGB(x, y); - if ((pixel >>> 24) == 0xFF && (pixel & 0x00FFFFFF) == expected) { - count++; - } - } - } - return count; - } - - private static int countArgb(BufferedImage image, Color color) { - int expected = color.getRGB(); - int count = 0; - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - if (image.getRGB(x, y) == expected) count++; - } - } - return count; - } - - private static int countOpaqueRgbNear(BufferedImage image, Color color, int tolerance) { - int count = 0; - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - Color pixel = new Color(image.getRGB(x, y), true); - if (pixel.getAlpha() == 0xFF - && Math.abs(pixel.getRed() - color.getRed()) <= tolerance - && Math.abs(pixel.getGreen() - color.getGreen()) <= tolerance - && Math.abs(pixel.getBlue() - color.getBlue()) <= tolerance) { - count++; - } - } - } - return count; - } - - private static List opaqueRgbMaskDifferences( - BufferedImage first, - BufferedImage second, - Color color, - int centerX, - int centerY, - int radius) { - int expected = color.getRGB() & 0x00FFFFFF; - java.util.ArrayList differences = new java.util.ArrayList<>(); - for (int y = centerY - radius; y <= centerY + radius; y++) { - for (int x = centerX - radius; x <= centerX + radius; x++) { - int firstPixel = first.getRGB(x, y); - int secondPixel = second.getRGB(x, y); - boolean firstMatches = (firstPixel >>> 24) == 0xFF && (firstPixel & 0x00FFFFFF) == expected; - boolean secondMatches = - (secondPixel >>> 24) == 0xFF && (secondPixel & 0x00FFFFFF) == expected; - if (firstMatches != secondMatches) differences.add(x + "," + y); - } - } - return differences; - } - - private static void closeExclusiveSessionForTest(Leelaz engine) throws Exception { - Field field = Leelaz.class.getDeclaredField("exclusiveGtpSession"); - field.setAccessible(true); - Object session = field.get(engine); - if (session == null) { - return; - } - Method cancelInitial = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpInitialStopTimeout", session.getClass()); - cancelInitial.setAccessible(true); - cancelInitial.invoke(engine, session); - Method cancelRelease = - Leelaz.class.getDeclaredMethod("cancelExclusiveGtpReleaseStopTimeout", session.getClass()); - cancelRelease.setAccessible(true); - cancelRelease.invoke(engine, session); - Method close = Leelaz.class.getDeclaredMethod("closeExclusiveGtpSession", session.getClass()); - close.setAccessible(true); - close.invoke(engine, session); - } - private static final class TestEnvironment implements AutoCloseable { private final Leelaz previousEngine; private final Board previousBoard; private final Config previousConfig; private final LizzieFrame previousFrame; private final Menu previousMenu; + private GtpConsolePane previousConsole; private final BottomToolbar previousToolbar; private final boolean previousEmpty; private final int previousWidth; private final int previousHeight; + private Object previousZobristTables; private final Leelaz engine; private final ByteArrayOutputStream output; private final LizzieFrame frame; @@ -722,20 +759,20 @@ static TestEnvironment open() throws Exception { boolean previousEmpty = EngineManager.isEmpty; int previousWidth = Board.boardWidth; int previousHeight = Board.boardHeight; + Method captureTables = featurecat.lizzie.rules.Zobrist.class.getDeclaredMethod("captureTables"); + captureTables.setAccessible(true); + Object previousZobristTables = captureTables.invoke(null); Board.boardWidth = 2; Board.boardHeight = 2; + featurecat.lizzie.rules.Zobrist.init(); Board board = allocate(TrackingBoard.class); board.setHistory(new BoardHistoryList(BoardData.empty(2, 2))); Config config = allocate(Config.class); config.analyzeUpdateIntervalCentisec = 10; config.trackingAnalysisMaxVisits = 100; config.showTrackingPointOutline = true; - config.trackingPointInteriorColor = new Color(255, 156, 156); - config.trackingPointInteriorOpacityPercent = 100; config.trackingPointOutlineOpacityPercent = 92; - config.trackingPointTextAutoColor = true; - config.trackingPointTextColor = Color.BLACK; config.currentKataGoRules = "chinese"; config.extraMode = ExtraMode.Normal; config.boardStyle = Config.BOARD_STYLE_JAPANESE; @@ -768,6 +805,7 @@ static TestEnvironment open() throws Exception { engine.isLoaded = true; engine.started = true; engine.isKatago = true; + engine.bestMovesEnginename = "KataGo"; engine.commandLists.addAll(List.of("stop", "kata-analyze")); setField(engine, Leelaz.class, "endGetCommandList", true); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -784,7 +822,7 @@ static TestEnvironment open() throws Exception { Lizzie.frame = frame; LizzieFrame.menu = allocate(SilentMenu.class); LizzieFrame.toolbar = allocate(BottomToolbar.class); - return new TestEnvironment( + TestEnvironment environment = new TestEnvironment( previousEngine, previousBoard, previousConfig, @@ -797,93 +835,117 @@ static TestEnvironment open() throws Exception { engine, output, frame); + environment.previousZobristTables = previousZobristTables; + environment.previousConsole = Lizzie.gtpConsole; + Lizzie.gtpConsole = allocate(SilentConsole.class); + return environment; } - void installStableReadBoard() throws Exception { + void installStableReadBoard(String... rows) throws Exception { ReadBoard readBoard = allocate(ReadBoard.class); - BoardHistoryNode node = Lizzie.board.getHistory().getCurrentHistoryNode(); - setField(readBoard, ReadBoard.class, "trackingEligibilityIdentity", new Object()); - setField(readBoard, ReadBoard.class, "trackingEligibilityRevision", 7L); - setField(readBoard, ReadBoard.class, "trackingEligibilityNode", node); - setField( - readBoard, - ReadBoard.class, - "trackingEligibilityBoardRevision", - Lizzie.board.getContextRevision()); - setField( - readBoard, - ReadBoard.class, - "trackingEligibilityReason", - ReadBoardTrackingEligibilityAdapter.Reason.STABLE); + if (engine.moveFocusCapability() == Leelaz.MoveFocusCapability.UNKNOWN) { + startSupportedAnalysis(); + } + for (String name : + List.of("conflictTracker", "historyJumpTracker", "localNavigationTracker")) { + Field field = ReadBoard.class.getDeclaredField(name); + field.setAccessible(true); + boolean navigation = name.equals("localNavigationTracker"); + java.lang.reflect.Constructor constructor = + navigation + ? field.getType().getDeclaredConstructor(java.util.function.BooleanSupplier.class) + : field.getType().getDeclaredConstructor(); + constructor.setAccessible(true); + field.set( + readBoard, + navigation + ? constructor.newInstance((java.util.function.BooleanSupplier) () -> true) + : constructor.newInstance()); + } + setField(readBoard, ReadBoard.class, "tempcount", new java.util.ArrayList()); frame.readBoard = readBoard; + for (String row : rows.length == 0 ? new String[] {"re=0,0", "re=0,0"} : rows) { + readBoard.parseLine(row); + } + readBoard.parseLine("end"); + completeSyncConfirmation(); + assertTrue( + readBoard.snapshot().stable(), readBoard.snapshot().reason().name() + " " + commands()); } - void installOrdinaryBestMove(String coordinate, int visits, double winrate, double scoreLead) { - Lizzie.board.getHistory().getCurrentHistoryNode().getData().bestMoves = - List.of(ordinaryMove(coordinate, visits, winrate, scoreLead, 0)); - } - - void installOrdinaryMoves( - String bestCoordinate, - int bestVisits, - double bestWinrate, - double bestScoreLead, - String otherCoordinate, - int otherVisits, - double otherWinrate, - double otherScoreLead) { - Lizzie.board.getHistory().getCurrentHistoryNode().getData().bestMoves = - List.of( - ordinaryMove(bestCoordinate, bestVisits, bestWinrate, bestScoreLead, 0), - ordinaryMove(otherCoordinate, otherVisits, otherWinrate, otherScoreLead, 1)); - } - - private static MoveData ordinaryMove( - String coordinate, int visits, double winrate, double scoreLead, int order) { - MoveData move = new MoveData(); - move.coordinate = coordinate; - move.playouts = visits; - move.winrate = winrate; - move.scoreMean = scoreLead; - move.order = order; - move.isKataData = true; - return move; - } - - void completeInitialFence(int id) throws Exception { - dispatch("=" + id); - processCommandResponse("=" + id); - dispatch(""); - } + private int respondedCommands; + private boolean streaming; - void completeFinalFence(int id) throws Exception { - dispatch(""); - dispatch("=" + id); - dispatch(""); + void startSupportedAnalysis() throws Exception { + assertTrue(engine.startMoveFocusProbeAfterInitialization()); + settleCommands(); + assertEquals(Leelaz.MoveFocusCapability.SUPPORTED, engine.moveFocusCapability()); } - void sendTrackingInfo(String line) throws Exception { - dispatch(line); + void settleCommands() throws Exception { + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + int quiet = 0; + while (System.nanoTime() < deadline) { + String[] lines = commands().split("\n"); + if (respondedCommands >= lines.length || lines[respondedCommands].isEmpty()) { + if (++quiet >= 5) return; + Thread.sleep(1); + continue; + } + quiet = 0; + String command = lines[respondedCommands++]; + if (streaming) { + dispatch(""); + streaming = false; + } + String id = command.matches("[0-9]+ .*" ) + ? command.substring(0, command.indexOf(' ')) : ""; + String body = id.isEmpty() ? command : command.substring(command.indexOf(' ') + 1); + dispatch("=" + id); + if (body.startsWith("kata-analyze")) streaming = true; + else dispatch(""); + } + throw new AssertionError("Command exchange did not settle: " + commands()); } private void dispatch(String line) throws Exception { - java.lang.reflect.Method method = - Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); + Method method = Leelaz.class.getDeclaredMethod("dispatchReaderLineForTest", String.class); method.setAccessible(true); method.invoke(engine, line); } - private void processCommandResponse(String line) throws Exception { - java.lang.reflect.Method method = - Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); + private void sendOrdinaryInfo(String line) throws Exception { + dispatch(line); + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (System.nanoTime() < deadline) { + var snapshot = frame.trackingDisplaySnapshot(); + boolean delivered = true; + for (MoveData move : engine.getBestMoves()) { + if (snapshot.selectedPoints().contains(move.coordinate) + && snapshot.visits().getOrDefault(move.coordinate, 0) < move.playouts) { + delivered = false; + } + } + if (delivered) return; + Thread.sleep(1); + } + throw new AssertionError("Focus progress did not consume the accepted ordinary payload"); } - private void sendOrdinaryInfo(String line) throws Exception { - java.lang.reflect.Method method = Leelaz.class.getDeclaredMethod("parseLine", String.class); - method.setAccessible(true); - method.invoke(engine, line); + void acknowledgePositionCommands() throws Exception { + settleCommands(); + } + + void completeSyncConfirmation() throws Exception { + java.util.concurrent.CompletableFuture confirmation = + ((TrackingBoard) Lizzie.board).syncConfirmation; + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + do { + acknowledgePositionCommands(); + if (confirmation == null || confirmation.isDone()) break; + Thread.sleep(1); + } while (System.nanoTime() < deadline); + if (confirmation != null) confirmation.get(1, java.util.concurrent.TimeUnit.SECONDS); } String commands() { @@ -892,16 +954,23 @@ String commands() { @Override public void close() throws Exception { - closeExclusiveSessionForTest(engine); + completeSyncConfirmation(); + frame.clearTrackingPoints(); + settleCommands(); Lizzie.leelaz = previousEngine; Lizzie.board = previousBoard; Lizzie.config = previousConfig; Lizzie.frame = previousFrame; LizzieFrame.menu = previousMenu; + Lizzie.gtpConsole = previousConsole; LizzieFrame.toolbar = previousToolbar; EngineManager.isEmpty = previousEmpty; Board.boardWidth = previousWidth; Board.boardHeight = previousHeight; + Method restoreTables = featurecat.lizzie.rules.Zobrist.class.getDeclaredMethod( + "restoreTables", previousZobristTables.getClass()); + restoreTables.setAccessible(true); + restoreTables.invoke(null, previousZobristTables); } } @@ -921,10 +990,31 @@ public void clearSelectImage() {} } private static final class TrackingBoard extends Board { + private Runnable afterSyncMove; + private java.util.concurrent.CompletableFuture syncConfirmation; + + @Override + public java.util.concurrent.CompletableFuture applyReadBoardSync( + Runnable localChanges, java.util.function.BooleanSupplier requiresConfirmation) { + syncConfirmation = super.applyReadBoardSync(localChanges, requiresConfirmation); + return syncConfirmation; + } + + @Override + public void placeForSync(int x, int y, Stone color, boolean newBranch) { + super.placeForSync(x, y, color, newBranch); + if (afterSyncMove != null) afterSyncMove.run(); + } + @Override public void clearAfterMove() {} } + private static final class SilentConsole extends GtpConsolePane { + private SilentConsole() { super((java.awt.Window) null); } + @Override public boolean isVisible() { return false; } + } + private static final class SilentMenu extends Menu { @Override public void toggleEngineMenuStatus(boolean isPondering, boolean isThinking) {} diff --git a/src/test/java/featurecat/lizzie/gui/web/WebTrialSingleStreamExclusionTest.java b/src/test/java/featurecat/lizzie/gui/web/WebTrialSingleStreamExclusionTest.java index d9d25bfa9..f67a36add 100644 --- a/src/test/java/featurecat/lizzie/gui/web/WebTrialSingleStreamExclusionTest.java +++ b/src/test/java/featurecat/lizzie/gui/web/WebTrialSingleStreamExclusionTest.java @@ -51,7 +51,7 @@ void setUp() throws Exception { previousManager = Lizzie.webBoardManager; previousConfig = Lizzie.config; previousBoard = Lizzie.board; - engine = trackingCapableEngine(); + engine = reusableLocalKatago(); output = new ByteArrayOutputStream(); setLeelazField(engine, "outputStream", new BufferedOutputStream(output)); overrideMutations = new AtomicInteger(); @@ -73,8 +73,8 @@ void tearDown() { } @Test - void activeTrackingWinsBeforeTrialWithoutAnyTrialOrTrackingMutation() throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); + void activeForegroundAnalysisWinsBeforeTrialWithoutAnyTrialMutation() throws Exception { + Leelaz.ForegroundAnalysisLeaseAcquisition foreground = activateForegroundAnalysis(engine); BoardHistoryNode anchor = anyNode(); EngineFollowController controller = new EngineFollowController(new NoOpEngineCommandSink()); manager.setEngineFollowController(controller); @@ -82,10 +82,9 @@ void activeTrackingWinsBeforeTrialWithoutAnyTrialOrTrackingMutation() throws Exc assertEquals( WebBoardManager.TrialEnterResult.ENGINE_BUSY, - manager.enterTrialWithResult("tracking-loser", anchor)); + manager.enterTrialWithResult("foreground-loser", anchor)); - assertTrue(tracking.lease().isOwned()); - assertEquals(Leelaz.TrackingReleaseDisposition.ACTIVE, tracking.lease().disposition()); + assertTrue(foreground.lease().isOwned()); assertEquals(bytesBeforeEnter, output.toString(StandardCharsets.UTF_8)); assertNull(manager.getTrialOwnerForTest()); assertTrue(anchor.variations.isEmpty()); @@ -93,49 +92,6 @@ void activeTrackingWinsBeforeTrialWithoutAnyTrialOrTrackingMutation() throws Exc assertEquals(0, overrideMutations.get()); } - @Test - void acquiringTrackingWinsBeforeTrialWithoutAnyTrialMutation() throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - BoardHistoryNode anchor = anyNode(); - String bytesBeforeEnter = output.toString(StandardCharsets.UTF_8); - - assertEquals(Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, tracking.availability()); - assertEquals( - WebBoardManager.TrialEnterResult.ENGINE_BUSY, - manager.enterTrialWithResult("tracking-acquiring", anchor)); - assertTrue(tracking.lease().isOwned()); - assertEquals(bytesBeforeEnter, output.toString(StandardCharsets.UTF_8)); - assertNull(manager.getTrialOwnerForTest()); - assertTrue(anchor.variations.isEmpty()); - assertEquals(0, overrideMutations.get()); - - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - } - - @Test - void releaseClosingTrackingWinsBeforeTrialWithoutAnyTrialMutation() throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); - BoardHistoryNode anchor = anyNode(); - assertTrue(tracking.lease().release()); - String bytesBeforeEnter = output.toString(StandardCharsets.UTF_8); - - assertEquals( - WebBoardManager.TrialEnterResult.ENGINE_BUSY, - manager.enterTrialWithResult("tracking-closing", anchor)); - assertTrue(tracking.lease().isOwned()); - assertEquals(bytesBeforeEnter, output.toString(StandardCharsets.UTF_8)); - assertNull(manager.getTrialOwnerForTest()); - assertTrue(anchor.variations.isEmpty()); - assertEquals(0, overrideMutations.get()); - - assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(dispatchExclusiveLine(engine, "=800000002")); - assertTrue(dispatchExclusiveLine(engine, "")); - assertFalse(tracking.lease().isOwned()); - } - @Test void trialWinnerExcludesLaterSingleStreamOwners() throws Exception { BoardHistoryNode anchor = anyNode(); @@ -190,7 +146,7 @@ void exitKeepsOwnersExcludedUntilControllerResyncSettles() throws Exception { void productionMessageEntryReturnsEngineBusyWithoutTrialMutation() throws Exception { Board board = new Board(); Lizzie.board = board; - Leelaz.TrackingStreamLeaseAcquisition tracking = activateTracking(engine); + Leelaz.ForegroundAnalysisLeaseAcquisition foreground = activateForegroundAnalysis(engine); RecordingWebBoardServer server = new RecordingWebBoardServer(); manager.attachWebSocketServer(server); String bytesBeforeEnter = output.toString(StandardCharsets.UTF_8); @@ -201,7 +157,7 @@ void productionMessageEntryReturnsEngineBusyWithoutTrialMutation() throws Except assertEquals("trial_denied", denied.getString("type")); assertEquals("engine_busy", denied.getString("reason")); assertEquals("", denied.getString("ownerClientId")); - assertTrue(tracking.lease().isOwned()); + assertTrue(foreground.lease().isOwned()); assertEquals(bytesBeforeEnter, output.toString(StandardCharsets.UTF_8)); assertNull(manager.getTrialOwnerForTest()); assertEquals(0, overrideMutations.get()); @@ -261,38 +217,47 @@ void inactiveTrialLeavesAutomaticRecoveryAdmissionUnchanged() throws Exception { } private void assertTrialExcludesEngineOwners() throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE, tracking.availability()); - assertNull(tracking.lease()); + Leelaz.ForegroundAnalysisLeaseAcquisition foreground = + engine.acquireForegroundAnalysisLease(line -> {}, lease -> {}, lease -> {}); assertEquals( Leelaz.ExclusiveGtpLeaseAvailability.APPLICATION_EXCLUSIVE_MODE, - engine.previewForegroundAnalysisLeaseAvailability()); + foreground.availability()); + assertNull(foreground.lease()); assertNull(engine.beginExclusiveGtpLifecycleReservation()); assertNull(engine.beginEngineModeReservation()); assertFalse(invokeCanArmReadBoardGma(engine)); assertFalse(invokeBeginReadBoardGmaSession(engine)); } - private static Leelaz trackingCapableEngine() throws Exception { + private static Leelaz reusableLocalKatago() throws Exception { Leelaz engine = new Leelaz(""); engine.started = true; engine.isLoaded = true; engine.isKatago = true; - engine.commandLists.addAll(List.of("stop", "boardsize", "komi", "kata-analyze")); + engine.commandLists.addAll( + List.of( + "stop", + "boardsize", + "komi", + "kata-get-rules", + "kata-set-rules", + "clear_board", + "play", + "set_position", + "kata-analyze")); setLeelazField(engine, "endGetCommandList", true); return engine; } - private static Leelaz.TrackingStreamLeaseAcquisition activateTracking(Leelaz engine) - throws Exception { - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); + private static Leelaz.ForegroundAnalysisLeaseAcquisition activateForegroundAnalysis( + Leelaz engine) throws Exception { + Leelaz.ForegroundAnalysisLeaseAcquisition foreground = + engine.acquireForegroundAnalysisLease(line -> {}, lease -> {}, lease -> {}); + assertEquals(Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, foreground.availability()); + assertNotNull(foreground.lease()); processCommandResponse(engine, "=800000000"); assertTrue(dispatchExclusiveLine(engine, "")); - assertTrue(tracking.lease().send("kata-analyze B 10")); - return tracking; + return foreground; } private static void setLeelazField(Leelaz engine, String name, Object value) throws Exception { diff --git a/src/test/java/featurecat/lizzie/rules/BoardDataAnalysisCacheTraceTest.java b/src/test/java/featurecat/lizzie/rules/BoardDataAnalysisCacheTraceTest.java index e885e998f..249364b38 100644 --- a/src/test/java/featurecat/lizzie/rules/BoardDataAnalysisCacheTraceTest.java +++ b/src/test/java/featurecat/lizzie/rules/BoardDataAnalysisCacheTraceTest.java @@ -57,6 +57,51 @@ void restoreGlobals() { Lizzie.frame = previousFrame; } + @Test + void ordinaryRootAdmissionComparesLegacyCountsAndKeepsSlotsIndependent() throws Exception { + installBoardGlobals(); + BoardData node = primaryNode(1000, 40, 1); + node.setPlayouts2(2000); + node.rootVisits2 = 2000; + Object source = new Object(); + var payload = featurecat.lizzie.analysis.KataGoAnalysisPayload.parse( + "info move D4 visits 900 winrate 0.6 pv D4 rootInfo visits 1500"); + assertEquals(BoardData.AnalysisAdoption.REJECTED, + node.adoptOrdinaryAnalysis(payload.moves, "KataGo", Lizzie.leelaz, + payload.totalVisits(), payload.rootVisits, null, source, false, false)); + assertEquals(1000, node.getPlayouts()); + assertEquals(-1, node.rootVisits); + payload = featurecat.lizzie.analysis.KataGoAnalysisPayload.parse( + "info move D4 visits 1100 winrate 0.6 pv D4 rootInfo visits 100"); + assertEquals(BoardData.AnalysisAdoption.FULL, + node.adoptOrdinaryAnalysis(payload.moves, "KataGo", Lizzie.leelaz, + payload.totalVisits(), payload.rootVisits, null, source, false, false)); + assertEquals(100, node.getPlayouts()); + assertEquals(100, node.rootVisits); + assertEquals(2000, node.rootVisits2); + var lower = featurecat.lizzie.analysis.KataGoAnalysisPayload.parse( + "info move C3 visits 50000 winrate 0.8 pv C3 rootInfo visits 50 ownership 0.5 -0.5"); + Object replacement = new Object(); + assertEquals(BoardData.AnalysisAdoption.OWNERSHIP_ONLY, + node.adoptOrdinaryAnalysis(lower.moves, "KataGo", Lizzie.leelaz, + lower.totalVisits(), lower.rootVisits, lower.ownership, replacement, false, false)); + assertEquals("D4", node.bestMoves.get(0).coordinate); + assertEquals(100, node.rootVisits); + assertEquals(List.of(0.5, -0.5), node.estimateArray); + node.isChanged2 = true; + assertEquals(BoardData.AnalysisAdoption.FULL, + node.adoptOrdinaryAnalysis(lower.moves, "KataGo", Lizzie.leelaz, + lower.totalVisits(), lower.rootVisits, null, replacement, true, false)); + assertEquals(50, node.rootVisits2); + assertEquals(100, node.rootVisits); + var unknown = featurecat.lizzie.analysis.KataGoAnalysisPayload.parse( + "info move C3 visits 90000 winrate 0.9 pv C3"); + assertEquals(BoardData.AnalysisAdoption.REJECTED, + node.adoptOrdinaryAnalysis(unknown.moves, "KataGo", Lizzie.leelaz, + unknown.totalVisits(), -1, null, replacement, false, false)); + assertEquals(100, node.rootVisits); + } + @Test void primaryHigherVisitsAcceptsAndEmitsStructuredDecisionWhenTraceIsOn() throws Exception { startFullTrace(); diff --git a/src/test/java/featurecat/lizzie/rules/BoardNodeKindHistoryPipelineTest.java b/src/test/java/featurecat/lizzie/rules/BoardNodeKindHistoryPipelineTest.java index 58c09d1bd..72128a659 100644 --- a/src/test/java/featurecat/lizzie/rules/BoardNodeKindHistoryPipelineTest.java +++ b/src/test/java/featurecat/lizzie/rules/BoardNodeKindHistoryPipelineTest.java @@ -6297,8 +6297,8 @@ public void maybeAjustPDA(BoardHistoryNode node) { } @Override - public boolean isPonderingOrWasPonderingBeforeTracking() { - return trackPonderCalls ? pretendingToPonder : super.isPonderingOrWasPonderingBeforeTracking(); + public boolean isPondering() { + return trackPonderCalls ? pretendingToPonder : super.isPondering(); } @Override diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserSemanticRoundTripTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserSemanticRoundTripTest.java index f17844ce4..77f1fab59 100644 --- a/src/test/java/featurecat/lizzie/rules/SGFParserSemanticRoundTripTest.java +++ b/src/test/java/featurecat/lizzie/rules/SGFParserSemanticRoundTripTest.java @@ -24,6 +24,53 @@ class SGFParserSemanticRoundTripTest { private static final int SIZE = 5; + @ParameterizedTest + @ValueSource(strings = {"LZ", "LZOP", "LZ2", "LZOP2"}) + void exactRootAndSparseCandidateMetricsSurviveAdoptAndRepeatedSave(String tag) throws Exception { + try (RulesLayerTestHarness ignored = RulesLayerTestHarness.open(SIZE)) { + boolean secondary = tag.endsWith("2"); + String sgf = "(;SZ[5]" + tag + + "[KataGo 45 12.3k 2.5 1.2 0 rootVisits=12345\n" + + "move C3 visits 50000 winrate 5500 order 10 edgeVisits 0 pv C3 D4])"; + for (int round = 0; round < 3; round++) { + BoardHistoryList history = SGFParser.parseSgf(sgf, true); + Lizzie.board.setHistory(history); + BoardData data = history.getStart().getData(); + assertEquals(12345, secondary ? data.getPlayouts2() : data.getPlayouts()); + assertEquals(12345, secondary ? data.rootVisits2 : data.rootVisits); + var move = (secondary ? data.bestMoves2 : data.bestMoves).get(0); + assertEquals(50000, move.playouts); + assertEquals(10, move.order); + assertEquals(0, move.edgeVisits); + assertEquals(List.of("C3", "D4"), move.variation); + BoardData copy = data.clone(); + assertEquals(12345, secondary ? copy.rootVisits2 : copy.rootVisits); + sgf = SGFParser.saveToString(false); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"LZ", "LZOP", "LZ2", "LZOP2"}) + void headerOnlyExactRootAndDowngradedLegacyRemainDistinct(String tag) throws Exception { + try (RulesLayerTestHarness ignored = RulesLayerTestHarness.open(SIZE)) { + boolean secondary = tag.endsWith("2"); + String sgf = "(;SZ[5]" + tag + "[KataGo 45 12.3k 2.5 1.2 0 rootVisits=12345])"; + for (int round = 0; round < 2; round++) { + BoardHistoryList history = SGFParser.parseSgf(sgf, true); + Lizzie.board.setHistory(history); + BoardData data = history.getStart().getData(); + assertEquals(12345, secondary ? data.getPlayouts2() : data.getPlayouts()); + assertEquals(12345, secondary ? data.rootVisits2 : data.rootVisits); + sgf = SGFParser.saveToString(false); + } + BoardData legacy = SGFParser.parseSgf( + "(;SZ[5]" + tag + "[KataGo 45 12k 2.5 1.2 0])", true).getStart().getData(); + assertEquals(12000, secondary ? legacy.getPlayouts2() : legacy.getPlayouts()); + assertEquals(-1, secondary ? legacy.rootVisits2 : legacy.rootVisits); + } + } + @Test void mainlineMovesPreserveOrderColorAndBoard() throws Exception { try (RulesLayerTestHarness ignored = RulesLayerTestHarness.open(SIZE)) { diff --git a/src/test/java/featurecat/lizzie/util/KataGoRuntimeHelperBenchmarkLeaseTest.java b/src/test/java/featurecat/lizzie/util/KataGoRuntimeHelperBenchmarkLeaseTest.java index d6d9cb30d..9f1c05d69 100644 --- a/src/test/java/featurecat/lizzie/util/KataGoRuntimeHelperBenchmarkLeaseTest.java +++ b/src/test/java/featurecat/lizzie/util/KataGoRuntimeHelperBenchmarkLeaseTest.java @@ -27,7 +27,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -405,14 +404,17 @@ void activeForegroundLeaseRejectsBenchmarkWithoutChangingPauseState() throws Exc Config config = ConfigTestHelper.createForTests(Files.createTempDirectory("katago-benchmark-lease")); Leelaz engine = reusableKatagoEngine(); + Leelaz.ForegroundAnalysisLease foregroundLease = null; try { Lizzie.config = config; Lizzie.leelaz = engine; Lizzie.frame = null; config.showPonderLimitedTips = true; - assertEquals( - Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, - engine.beginExclusiveGtpSession(line -> {}, () -> {}, () -> {})); + Leelaz.ForegroundAnalysisLeaseAcquisition foreground = + engine.acquireForegroundAnalysisLease(line -> {}, lease -> {}, lease -> {}); + assertEquals(Leelaz.ExclusiveGtpLeaseAvailability.AVAILABLE, foreground.availability()); + assertNotNull(foreground.lease()); + foregroundLease = foreground.lease(); KataGoRuntimeHelper.BenchmarkPauseResult result = KataGoRuntimeHelper.pauseCurrentAnalysisForBenchmark(); @@ -424,7 +426,9 @@ void activeForegroundLeaseRejectsBenchmarkWithoutChangingPauseState() throws Exc assertTrue(engine.isLoaded()); assertTrue(engine.isStarted()); } finally { - engine.endExclusiveGtpSession(); + if (foregroundLease != null) { + engine.endExclusiveGtpSession(); + } Lizzie.config = previousConfig; Lizzie.leelaz = previousEngine; Lizzie.frame = previousFrame; @@ -499,62 +503,6 @@ void acceptedBenchmarkPauseBlocksLocalGtpConfigurationProbe() throws Exception { } } - @Test - void benchmarkPauseClaimsActiveTrackingAndRestoresSavedPonderIntentInOneClick() throws Exception { - try (BenchmarkEnvironment environment = new BenchmarkEnvironment(1)) { - RecordingBenchmarkLeelaz engine = environment.engine(0); - ByteArrayOutputStream output = installOutput(engine); - engine.Pondering(); - engine.ponderingCallCount = 0; - Leelaz.TrackingStreamLeaseAcquisition tracking = - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - - KataGoRuntimeHelper.BenchmarkPauseResult pause = environment.pause(0); - - assertTrue(pause.accepted()); - assertTrue(pause.analysisWasPondering()); - int pausePonderCalls = engine.ponderingCallCount; - assertEquals(Leelaz.TrackingReleaseDisposition.CLEARED, tracking.lease().disposition()); - assertEquals("800000000 stop\n800000001 stop\n", output.toString(StandardCharsets.UTF_8)); - - assertTrue(dispatchExclusiveLine(engine, "=800000001")); - assertTrue(dispatchExclusiveLine(engine, "")); - KataGoRuntimeHelper.restoreAnalysisAfterBenchmark(pause.analysisWasPondering()); - assertEquals(2, engine.reservationAttempts); - assertEquals(1, engine.restartCount); - assertEquals(pausePonderCalls + 1, engine.ponderingCallCount); - } - } - - @Test - void nonShutdownBenchmarkKeepsTrackingPausedAndRestoresSavedPonderIntent() throws Exception { - try (BenchmarkEnvironment environment = new BenchmarkEnvironment(1)) { - RecordingBenchmarkLeelaz engine = environment.engine(0); - installOutput(engine); - engine.Pondering(); - engine.acquireTrackingStreamLease(line -> {}, lease -> {}, lease -> {}); - processCommandResponse(engine, "=800000000"); - assertTrue(dispatchExclusiveLine(engine, "")); - EngineManager.isEmpty = true; - - KataGoRuntimeHelper.BenchmarkPauseResult pause = environment.pause(0); - - assertTrue(pause.accepted()); - assertTrue(pause.analysisWasPondering()); - assertFalse(pause.computeIsolated()); - assertFalse( - engine.isPondering(), "benchmark must not restart ponder while tracking settles."); - - assertTrue(dispatchExclusiveLine(engine, "=800000001")); - assertTrue(dispatchExclusiveLine(engine, "")); - KataGoRuntimeHelper.restoreAnalysisAfterBenchmark(pause.analysisWasPondering()); - - assertTrue(engine.isPondering()); - assertFalse(engine.hasExclusiveGtpWorkInProgress()); - } - } @Test void shutdownWaitReportsOnlyConfirmedIsolation() throws Exception { @@ -1003,13 +951,6 @@ private static T allocate(Class type) throws Exception { return (T) UnsafeHolder.UNSAFE.allocateInstance(type); } - private static ByteArrayOutputStream installOutput(Leelaz engine) throws Exception { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Field field = Leelaz.class.getDeclaredField("outputStream"); - field.setAccessible(true); - field.set(engine, new BufferedOutputStream(output)); - return output; - } private static void awaitRestartSettlement(Leelaz engine) { long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); @@ -1044,11 +985,6 @@ private static void setCapabilityDiscoveryComplete(Leelaz engine, boolean comple } } - private static boolean dispatchExclusiveLine(Leelaz engine, String line) throws Exception { - Method method = Leelaz.class.getDeclaredMethod("dispatchExclusiveGtpLine", String.class); - method.setAccessible(true); - return (boolean) method.invoke(engine, line); - } private static void processCommandResponse(Leelaz engine, String line) throws Exception { Method method = Leelaz.class.getDeclaredMethod("processCommandResponseLine", String.class); diff --git a/src/test/resources/analysis/katago-root-focus-info.txt b/src/test/resources/analysis/katago-root-focus-info.txt new file mode 100644 index 000000000..484765522 --- /dev/null +++ b/src/test/resources/analysis/katago-root-focus-info.txt @@ -0,0 +1 @@ +info move E4 visits 20 edgeVisits 20 utility -0.0915208 winrate 0.463669 scoreMean -0.296807 scoreStdev 13.5769 scoreLead -0.296807 scoreSelfplay -0.669665 prior 0.0967378 lcb 0.441797 utilityLcb -0.152763 weight 20 order 0 pv E4 E6 C5 C6 B6 D5 D4 info move F5 visits 19 edgeVisits 19 utility -0.0938085 winrate 0.463708 scoreMean -0.356633 scoreStdev 13.5183 scoreLead -0.356633 scoreSelfplay -0.628081 prior 0.112006 lcb 0.437567 utilityLcb -0.167002 weight 19 order 1 pv F5 D5 E3 D3 D2 E4 F4 info move E5 visits 18 edgeVisits 18 utility -0.121449 winrate 0.445123 scoreMean -0.539178 scoreStdev 13.6171 scoreLead -0.539178 scoreSelfplay -1.35072 prior 0.269292 lcb 0.422055 utilityLcb -0.186038 weight 18 order 2 pv E5 E3 G4 C4 info move E6 visits 12 edgeVisits 12 utility -0.0967242 winrate 0.458091 scoreMean -0.482197 scoreStdev 13.8166 scoreLead -0.482197 scoreSelfplay -0.767707 prior 0.124995 lcb 0.427496 utilityLcb -0.182391 weight 12 order 3 pv E6 E4 C5 C4 B4 D5 D6 info move D5 visits 6 edgeVisits 6 utility -0.117584 winrate 0.456263 scoreMean -0.365468 scoreStdev 13.4187 scoreLead -0.365468 scoreSelfplay -0.818276 prior 0.0765883 lcb 0.38335 utilityLcb -0.32174 weight 6 order 4 pv D5 F5 E7 F7 F8 info move F4 visits 6 edgeVisits 6 utility -0.118356 winrate 0.457251 scoreMean -0.631535 scoreStdev 12.9994 scoreLead -0.631535 scoreSelfplay -0.976576 prior 0.0699181 lcb 0.383528 utilityLcb -0.324779 weight 5.96076 order 5 pv F4 D6 C4 G6 info move F6 visits 6 edgeVisits 6 utility -0.131301 winrate 0.450815 scoreMean -0.694211 scoreStdev 12.9469 scoreLead -0.694211 scoreSelfplay -0.964098 prior 0.0741216 lcb 0.375212 utilityLcb -0.342989 weight 6 order 6 pv F6 D4 F4 D7 info move D4 visits 6 edgeVisits 6 utility -0.113417 winrate 0.458595 scoreMean -0.571439 scoreStdev 13.0216 scoreLead -0.571439 scoreSelfplay -0.866004 prior 0.0547762 lcb 0.384573 utilityLcb -0.320676 weight 6 order 7 pv D4 F6 D7 F4 info move D6 visits 6 edgeVisits 6 utility -0.119245 winrate 0.455044 scoreMean -0.693939 scoreStdev 13.0661 scoreLead -0.693939 scoreSelfplay -0.849603 prior 0.0521849 lcb 0.380117 utilityLcb -0.329041 weight 6 order 8 pv D6 F4 F6 C4 info move G7 visits 381 edgeVisits 0 utility -0.312035 winrate 0.368548 scoreMean -2.05289 scoreStdev 13.6366 scoreLead -2.05289 scoreSelfplay -2.23272 prior 0.000535624 lcb -4.63145 utilityLcb -14 weight 380.472 order 9 pv G7 F6 C4 G6 F7 E6 D7 E7 E8 D8 C8 F8 info move C3 visits 368 edgeVisits 0 utility -0.333332 winrate 0.358297 scoreMean -2.41376 scoreStdev 14.2414 scoreLead -2.41376 scoreSelfplay -2.54984 prior 0.000501455 lcb -4.6417 utilityLcb -14 weight 367.921 order 10 pv C3 D4 C4 D6 G6 F4 F7 C5 rootInfo visits 100 utility -0.104704 winrate 0.457659 scoreMean -0.44165 scoreStdev 13.5075 scoreLead -0.44165 scoreSelfplay -0.852823 weight 99.9608 rawStWrError -1 rawStScoreError -1 rawVarTimeLeft -1