* fix(windows,engine): 提交键回复等待不再因超时重置会话或丢字 服务端按键处理偶发数百毫秒尖峰时,提交键(空格顶首选 / 确定选词 / 标点顶高亮候选) 的回复等待会走到会话内那条 300ms 同步读;一超时就被判成 TransportUnavailable → DeliveryAmbiguous → Transport 完整重置(丢键 + 吞队列 + 清组合 + 轮换 epoch), 用户看到的是整段已输入内容消失。 - 删除 FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS 那条破坏性兜底读。会话侧改为非阻塞探测, 拿不到回复返回「未就绪」,由 session 自身在 DoEditSession 内交回,沿用同一个 FANY_IME_COMMIT_REPLY_TIMEOUT_MS 分片轮询窗口,不重开窗口、不新造等待原语。 - 交回必须发生在 session 内部:RequestEditSession 用 TF_ES_ASYNCDONTCARE,TSF 异步授权 时调用方读不到 session 的 HRESULT;若靠调用方读返回值,该键会既不丢也不提交, deferred 闸门再不打开(后续按键全部只入队)。也不能改用 TF_ES_SYNC(WinWord 上不成立)。 - 真失败(写失败 / 读失败 / 坏帧 / 字节数不符 / 无 request id / 无管道)仍判 Transport 重置。 - 预取覆盖面扩到 candidate 类 finalize(空格顶首选等),堵住原先只能靠那条 300ms 兜底读的路径。 - 新增按键级延迟诊断 bench_key_latency_diag(build 里有同名 exe 但源码已不在树), 以及两份对照测试:commit_reply_not_ready(回复三态与交回状态机)、 candidate_consistency(同配置下候选集合/顺序/分页逐字一致,引擎侧护栏)。 实测(bench_key_latency_diag):稳态逐键 median 0.6~0.8ms / max 12.5ms,尖峰指向内存 压力下的页回收而非稳态算力。在「结果完全不变」的约束下,预热 / 缓存 / 投机预解码均无效 或反向,故本次不改引擎打分路径。 * fix(windows): 直接标点提交会话补齐未就绪交回,并写准窗口沿用边界 审查 `7aca5e5d` 发现的问题: - `CPunctuationCommitEditSession` 在 `FANY_S_COMMIT_REPLY_NOT_READY` 时只抑制了 token 结算(`applied` / `_FailDeferredKey` 都不走),**没有**像 `CKeyHandlerEditSession` 那样把 请求交回 `_PollCommitReply`。一旦走到这条分支——`_HandleCompositionPunctuation` 的高亮候选 分支要求 `_candidateMode != CANDIDATE_NONE`,而派发该会话的条件是 `_candidateMode == CANDIDATE_NONE`,两者在 session 执行时才求值——deferred token 会永久留在 飞行中,后续所有按键只入队不处理,正是本 PR 在别处修掉的失效模式。现在两处都交回, 并把原因写进注释,避免下一个人再照半个写法抄一遍。 - `_HandOffUnreadyCommitReply` 的两处注释与实现不符或不够具体:交回沿用第一片已经开始的窗口; 只有从未开窗的路径(标点处理函数在派发时高亮候选条件为假、session 执行时条件成立)才在此 开窗,那种情况键一点没等过、`waitedMs` 为 0。另一支(无法保活 → `_FailUnansweredCommitKey`) 只在请求不属于协作轮询时到达,实务上即 `commitReplyMessage == 0`:派发没能用消息窗, 没有可交回的对象。 签名相应扩展:`_RequestDirectPunctuationEditSession` 新增 `commitReplyMessage` / `commitReplyDeadlineTick`,`CPunctuationCommitEditSession` 携带它们。没有行为回退,两个新参数 默认 0 时与改动前一致(而那正是「无消息窗」的既有降级路径)。 验证:windows x64 Release 构建通过,`ctest` 17/17 通过,`clang-format 18.1.8 --dry-run --Werror --style=file` 对本次涉及文件全过。 * fix(windows): 把 _HandOffUnreadyCommitReply 移到公共区,修 CI 编译失败 CI 的 Windows x64 与 Windows Win32 两条腿都挂在同一处编译错误: MetasequoiaIME_AsyncRequests.cpp: error C2248: “CMetasequoiaIME::_HandOffUnreadyCommitReply”: 无法访问 private 成员 上一提交给 CPunctuationCommitEditSession 补「未就绪交回」时调用了这个成员,但它在 private 区。CKeyHandlerEditSession 能调是因为它在 MetasequoiaIME.h 里被显式声明为 friend;而 CPunctuationCommitEditSession 定义在 MetasequoiaIME_AsyncRequests.cpp 的匿名命名空间里 (该文件 31-254 行),头文件里的 friend 声明指不到它,加 friend 也无效。 处置:把该声明移到公共区,与同样由 session 调用的 _FailDeferredKey、_CompleteDeferredKeyReplay、 _HandleCompositionPunctuation 并列,并在注释里写明「它必须公共」的原因,免得下次有人顺手挪回私区。 仅可见性与注释,行为零变化。 验证(triplet 与 CI 一致,且这次看退出码、不截断构建输出): - x64-windows-static:cmake --build build --config Release --parallel 退出码 0; ctest --no-tests=error 17/17。 - x86-windows-static:cmake --build build32 --config Release --parallel 退出码 0; ctest --no-tests=error 17/17。 - clang-format 18.1.8 --dry-run --Werror --style=file 对本次改动文件通过。 另:上一轮「构建通过」是我误报——当时用 Select-String 过滤构建输出又加了 -Last 8,错误行被截掉, 且没有检查 $LASTEXITCODE;ctest 过的是测试目标,与 DLL 编译失败并不冲突。 * fix(windows): 提交回复交回后轮询不再停摆,同步兜底保留有界等待 - 轮询定时器按请求 id 判断是否仍在等待:session 交回时换了新 token, 按旧 token 查会误判为已结算并 KillTimer,交回的请求从此无人再分发。 - 没有轮询可交回的 session(同步 _InvokeKeyHandler 路径)恢复 FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS 的单次有界等待,不再一读不到就丢键重置。 - 确定并输入在提交回复为传输失败时不再继续键入字符并返回 S_OK。 - session 交回占用新槽位时同样受 kMaxPendingAsyncKeys 约束。 - 修正注释:未就绪由 session 自己的完成钩子交回,不是消息处理函数。 - 测试:覆盖轮询分发、等待上限与满表交回;SAL 剥离按最长前缀匹配; 引擎一致性用例③不再清缓存,否则测不到跨输入的缓存泄漏。 --------- Co-authored-by: fanlusky <1730976608@qq.com>
147 lines
5.5 KiB
Python
147 lines
5.5 KiB
Python
"""Compile the production commit-reply decision paths against a standalone service fixture.
|
|
|
|
The TSF service, its message window and the reply pipe cannot be instantiated without a host,
|
|
so the paths that decide what an *unready* committing reply costs are extracted verbatim
|
|
instead of copied into the test: the three-state reply read, the session's wait bound, the
|
|
hand-off that keeps a request alive, the poll timer pass, and both session completion hooks. Only the pipe probe, the message window and the
|
|
clock remain test doubles. Regenerate whenever one of the extracted functions changes, so a
|
|
removed step cannot hide behind a copy here.
|
|
|
|
Two headers are produced, because the test's service stub has to declare the extracted
|
|
`AsyncKeyRequest` before it can own a request table: `<...>_types_source.h` with the types the
|
|
stub needs, then `<...>_source.h` with the extracted definitions.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
|
|
TOKENS = re.compile(
|
|
r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|[{}]',
|
|
re.DOTALL,
|
|
)
|
|
|
|
# SAL annotations travel in the production signatures; the fixture has no sal.h.
|
|
# Longest spellings first: alternation takes the first match, so `In` ahead of
|
|
# `Inout` would leave `out_` behind.
|
|
SAL = re.compile(
|
|
r"\b_(?:Outptr_result_maybenull_|Inout_opt_|Inout|In_opt_|In_z_|In|Out_opt_|Out_z_|Out)_?[ \t]*"
|
|
)
|
|
|
|
|
|
def find_body(source, marker, after=0):
|
|
"""Return (signature, body) for the definition the marker names."""
|
|
start = source.index(marker, after)
|
|
opening = None
|
|
depth = 0
|
|
for token in TOKENS.finditer(source, start):
|
|
if token.group() == "{":
|
|
if opening is None:
|
|
opening = token.start()
|
|
depth += 1
|
|
elif token.group() == "}":
|
|
depth -= 1
|
|
if opening is not None and depth == 0:
|
|
return SAL.sub("", source[start:opening]).strip(), source[
|
|
opening : token.end()
|
|
]
|
|
raise ValueError(f"Unclosed production definition: {marker}")
|
|
|
|
|
|
def find_declaration(source, marker):
|
|
"""Return the verbatim `marker ... ;` declaration."""
|
|
start = source.index(marker)
|
|
return source[start : source.index(";", start) + 1]
|
|
|
|
|
|
def generate(
|
|
ipc_header,
|
|
ipc_source,
|
|
ime_header,
|
|
async_source,
|
|
session_source,
|
|
types_output,
|
|
source_output,
|
|
):
|
|
ipc_h = Path(ipc_header).read_text(encoding="utf-8")
|
|
ipc_cpp = Path(ipc_source).read_text(encoding="utf-8")
|
|
ime_h = Path(ime_header).read_text(encoding="utf-8")
|
|
async_cpp = Path(async_source).read_text(encoding="utf-8")
|
|
session_cpp = Path(session_source).read_text(encoding="utf-8")
|
|
|
|
banner = "// Generated from the production commit-reply paths; do not edit.\n"
|
|
|
|
status_signature, status_body = find_body(ipc_h, "enum class CommitReplyWaitStatus")
|
|
request_signature, request_body = find_body(ime_h, "struct AsyncKeyRequest")
|
|
|
|
types = banner
|
|
types += f"{status_signature}\n{status_body};\n"
|
|
types += (
|
|
find_declaration(ipc_h, "constexpr DWORD FANY_IME_COMMIT_REPLY_TIMEOUT_MS")
|
|
+ "\n"
|
|
)
|
|
types += (
|
|
find_declaration(ipc_h, "constexpr DWORD FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS")
|
|
+ "\n"
|
|
)
|
|
types += (
|
|
find_declaration(ipc_h, "constexpr HRESULT FANY_S_COMMIT_REPLY_NOT_READY")
|
|
+ "\n"
|
|
)
|
|
types += (
|
|
find_declaration(ime_h, "static constexpr size_t kMaxPendingAsyncKeys")
|
|
+ "\n"
|
|
)
|
|
types += (
|
|
find_declaration(ipc_h, "constexpr HRESULT FANY_E_COMMIT_REPLY_AMBIGUOUS")
|
|
+ "\n"
|
|
)
|
|
types += f"{request_signature}\n{request_body};\n"
|
|
Path(types_output).write_text(types, encoding="utf-8")
|
|
|
|
# The punctuation commit session precedes the Completion struct it defines; anchoring the
|
|
# search there keeps the extraction on that struct if another `struct Completion` appears.
|
|
punctuation_session = async_cpp.index("class CPunctuationCommitEditSession")
|
|
reply_session = session_cpp.index("STDAPI CKeyHandlerEditSession::DoEditSession")
|
|
|
|
poll_signature, poll_body = find_body(
|
|
async_cpp, "bool IsCommitReplyPollMessage(UINT message)"
|
|
)
|
|
requeue_signature, requeue_body = find_body(
|
|
async_cpp, "bool CMetasequoiaIME::_RequeueAsyncKeyRequest("
|
|
)
|
|
handoff_signature, handoff_body = find_body(
|
|
async_cpp, "void CMetasequoiaIME::_HandOffUnreadyCommitReply("
|
|
)
|
|
wait_signature, wait_body = find_body(
|
|
async_cpp, "DWORD CMetasequoiaIME::_SessionCommitReplyWaitMs()"
|
|
)
|
|
dispatch_signature, dispatch_body = find_body(
|
|
async_cpp, "void CMetasequoiaIME::_DispatchPendingCommitReplyPolls()"
|
|
)
|
|
read_signature, read_body = find_body(
|
|
ipc_cpp, "CommitReplyWaitStatus TryReadCommitReplyFromServerPipe("
|
|
)
|
|
completion_signature, completion_body = find_body(
|
|
async_cpp, "struct Completion", punctuation_session
|
|
)
|
|
replay_signature, replay_body = find_body(
|
|
session_cpp, "struct DeferredReplayCompletion", reply_session
|
|
)
|
|
|
|
source = banner
|
|
source += f"{poll_signature}\n{poll_body}\n"
|
|
source += f"{requeue_signature}\n{requeue_body}\n"
|
|
source += f"{handoff_signature}\n{handoff_body}\n"
|
|
source += f"{wait_signature}\n{wait_body}\n"
|
|
source += f"{dispatch_signature}\n{dispatch_body}\n"
|
|
source += f"{read_signature}\n{read_body}\n"
|
|
source += f"{completion_signature}\n{completion_body};\n"
|
|
source += f"{replay_signature}\n{replay_body};\n"
|
|
Path(source_output).write_text(source, encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
generate(*sys.argv[1:])
|