From 8b65243b113f545959d20ba2b3251d1dbb83bfc5 Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Sun, 27 Sep 2026 16:59:31 +0800 Subject: [PATCH] =?UTF-8?q?perf(windows):=20=E7=84=A6=E7=82=B9=E6=BF=80?= =?UTF-8?q?=E6=B4=BB=E4=B8=8E=20worker=20=E7=AE=A1=E9=81=93=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=94=B9=E4=B8=BA=E4=BA=8B=E4=BB=B6=E5=94=A4=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切换焦点后,EnsureNamedpipeFocusSessionActivated 等 worker 管道回显 focus token 时用 Sleep(1) 轮询,最长 150 ms。Sleep(1) 按系统时钟粒度向上取整(默认约 15.6 ms),而回显通常不到 1 ms 就到了,所以每次切换焦点都白等一个时钟周期。 冷激活、Server 重启或重连之后,IPC worker 线程没有管道时每 50 ms 才检查一次 新发布的句柄,回显又在管道里多躺最多 50 ms。两处都在宿主 UI 线程的首键路径上。 - 新增两个自动重置事件:worker 记下确认后 SetEvent(_workerAckEvent); PublishWorkerPipeHandleIfChanged 发布非空句柄后 SetEvent(_workerPipePublishedEvent)。 - 等确认的循环改为 WaitForSingleObject(ack, 剩余时间)。检查逻辑、令牌比对和 150 ms 上限不变;每次唤醒都先重新检查,worker 在 release 写入之后才发信号, 不会丢唤醒,残留信号只多一轮检查。与 Sleep 一样不泵消息,不会重入 TSF 回调。 - worker 无管道时改为 WaitForMultipleObjects({stop, published}, 50),stop 在 下标 0 优先;50 ms 超时保留为兜底。 - 事件经新增的 BindNamedpipeWakeEvents 绑定到线程本地,UnbindNamedpipeFocusState 一并清除。在 IPC 线程启动前创建、join 之后关闭;Deactivate 关闭前先解绑, 其后的 _EndComposition 若再进等待循环会退回轮询,不会等已关闭的句柄。创建 失败或等待失败时退回原来的 Sleep(1)。 - 不动 PeekNamedPipe 存活检查,也不用 timeBeginPeriod(会改宿主进程的全局状态)。 --- windows/src/IME/MetasequoiaIME.cpp | 27 +++++++++++++ windows/src/IME/MetasequoiaIME.h | 9 +++++ windows/src/IME/MetasequoiaIME_IpcWorker.cpp | 12 +++++- windows/src/IPC/Ipc.cpp | 41 +++++++++++++++++++- windows/src/IPC/Ipc.h | 8 ++++ 5 files changed, 94 insertions(+), 3 deletions(-) diff --git a/windows/src/IME/MetasequoiaIME.cpp b/windows/src/IME/MetasequoiaIME.cpp index 4e4b6f0b..a065a2ca 100644 --- a/windows/src/IME/MetasequoiaIME.cpp +++ b/windows/src/IME/MetasequoiaIME.cpp @@ -263,6 +263,8 @@ CMetasequoiaIME::CMetasequoiaIME() _hToTsfWorkerThreadPipe.store(nullptr); _workerPipeGeneration.store(0); _ipcStopEvent = nullptr; + _workerAckEvent = nullptr; + _workerPipePublishedEvent = nullptr; _shouldStopIpcThread = false; _ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; _ipcConsecutiveFailures = 0; @@ -369,6 +371,8 @@ CMetasequoiaIME::~CMetasequoiaIME() CloseHandle(_ipcStopEvent); _ipcStopEvent = nullptr; } + // The owner-aware unbind above already cleared the TLS copies. + _CloseIpcWakeEvents(); } uint64_t CMetasequoiaIME::_CaptureFocusSessionToken() const @@ -796,6 +800,10 @@ STDAPI CMetasequoiaIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClient { goto ExitError; } + // Optional: if either fails the waits fall back to the original polling. + _workerAckEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + _workerPipePublishedEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + BindNamedpipeWakeEvents(this, _workerAckEvent, _workerPipePublishedEvent); _shouldStopIpcThread.store(false); _pIpcThread = new std::thread(IpcWorkerThread, this); @@ -927,6 +935,11 @@ STDAPI CMetasequoiaIME::Deactivate() CloseHandle(_ipcStopEvent); _ipcStopEvent = nullptr; } + // _EndComposition below can still reach the focus-activation wait on this + // thread; unbind first so it falls back to polling instead of waiting on a + // closed (or recycled) handle. + BindNamedpipeWakeEvents(this, nullptr, nullptr); + _CloseIpcWakeEvents(); // Stop callbacks from the previously focused top context before tearing // down the engine and thread manager. Context stack changes can otherwise // re-enter a half-deactivated service. @@ -1028,6 +1041,20 @@ STDAPI CMetasequoiaIME::Deactivate() return S_OK; } +void CMetasequoiaIME::_CloseIpcWakeEvents() +{ + if (_workerAckEvent) + { + CloseHandle(_workerAckEvent); + _workerAckEvent = nullptr; + } + if (_workerPipePublishedEvent) + { + CloseHandle(_workerPipePublishedEvent); + _workerPipePublishedEvent = nullptr; + } +} + //+--------------------------------------------------------------------------- // // _WakeServerIfNeeded diff --git a/windows/src/IME/MetasequoiaIME.h b/windows/src/IME/MetasequoiaIME.h index 5db6a39d..e779d7ca 100644 --- a/windows/src/IME/MetasequoiaIME.h +++ b/windows/src/IME/MetasequoiaIME.h @@ -779,6 +779,15 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, std::atomic _hToTsfWorkerThreadPipe; std::atomic _workerPipeGeneration; HANDLE _ipcStopEvent; + // Auto-reset wake-ups that replace polling (see BindNamedpipeWakeEvents): the + // worker signals _workerAckEvent after recording the focus acknowledgement, + // and the UI thread signals _workerPipePublishedEvent when it publishes a + // worker pipe. Created before the IPC thread starts, closed after it joins. + HANDLE _workerAckEvent; + HANDLE _workerPipePublishedEvent; + // Closes both wake-up events. Only after the IPC thread has joined and the + // TLS bindings no longer point at them. + void _CloseIpcWakeEvents(); std::atomic _shouldStopIpcThread; UINT _ipcReconnectDelayMs; UINT _ipcConsecutiveFailures; diff --git a/windows/src/IME/MetasequoiaIME_IpcWorker.cpp b/windows/src/IME/MetasequoiaIME_IpcWorker.cpp index f8cd1d96..5ff13830 100644 --- a/windows/src/IME/MetasequoiaIME_IpcWorker.cpp +++ b/windows/src/IME/MetasequoiaIME_IpcWorker.cpp @@ -281,7 +281,12 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) const UINT workerGeneration = pIME->_workerPipeGeneration.load(std::memory_order_acquire); if (!workerPipe || workerPipe == INVALID_HANDLE_VALUE) { - if (pIME->_ipcStopEvent && WaitForSingleObject(pIME->_ipcStopEvent, 50) == WAIT_OBJECT_0) + // Wake as soon as the UI thread publishes a pipe instead of on the + // next 50 ms tick; the timeout stays as a fallback. Stop is index 0 + // so it wins when both are signalled. + HANDLE waitHandles[] = {pIME->_ipcStopEvent, pIME->_workerPipePublishedEvent}; + const DWORD waitCount = pIME->_workerPipePublishedEvent ? 2 : 1; + if (pIME->_ipcStopEvent && WaitForMultipleObjects(waitCount, waitHandles, FALSE, 50) == WAIT_OBJECT_0) { return; } @@ -515,6 +520,11 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) { pIME->_acknowledgedWorkerFocusToken.store(focusToken, std::memory_order_release); pIME->_workerCommitReady.store(true, std::memory_order_release); + // Wake the UI thread waiting in EnsureNamedpipeFocusSessionActivated. + if (pIME->_workerAckEvent) + { + SetEvent(pIME->_workerAckEvent); + } } } else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CommitCurCandidate) diff --git a/windows/src/IPC/Ipc.cpp b/windows/src/IPC/Ipc.cpp index 9a527f4a..bb13096f 100644 --- a/windows/src/IPC/Ipc.cpp +++ b/windows/src/IPC/Ipc.cpp @@ -45,6 +45,8 @@ static thread_local std::atomic *boundWorkerCommitReady = nullptr; static thread_local std::atomic *boundAcknowledgedWorkerFocusToken = nullptr; static thread_local std::atomic *boundWorkerPipeHandle = nullptr; static thread_local std::atomic *boundWorkerPipeGeneration = nullptr; +static thread_local HANDLE boundWorkerAckEvent = nullptr; +static thread_local HANDLE boundWorkerPipePublishedEvent = nullptr; /* Data size transfered from Server process */ static const int ServerDtPipeDataSize = 512; @@ -243,6 +245,13 @@ void PublishWorkerPipeHandleIfChanged(HANDLE workerPipe) boundWorkerPipeGeneration->store(NextAtomicNonzeroToken(nextWorkerPipeGeneration), std::memory_order_release); } boundWorkerPipeHandle->store(workerPipe, std::memory_order_release); + // The IPC worker thread sleeps while it has no pipe; without this it only + // noticed the new handle on its next 50 ms timeout, and the FocusSessionReady + // echo the first key waits for sat unread in the pipe until then. + if (workerPipe && boundWorkerPipePublishedEvent) + { + SetEvent(boundWorkerPipePublishedEvent); + } } inline bool IsValidPipeHandle(HANDLE hPipeHandle) @@ -620,6 +629,16 @@ void BindNamedpipeFocusState(const void *owner, bool *focusResetPending, bool *a boundWorkerPipeGeneration = workerPipeGeneration; } +void BindNamedpipeWakeEvents(const void *owner, HANDLE workerAckEvent, HANDLE workerPipePublishedEvent) +{ + if (owner == nullptr || boundFocusStateOwner != owner) + { + return; + } + boundWorkerAckEvent = workerAckEvent; + boundWorkerPipePublishedEvent = workerPipePublishedEvent; +} + void UnbindNamedpipeFocusState(const void *owner) { if (owner == nullptr || boundFocusStateOwner != owner) @@ -636,6 +655,8 @@ void UnbindNamedpipeFocusState(const void *owner) boundAcknowledgedWorkerFocusToken = nullptr; boundWorkerPipeHandle = nullptr; boundWorkerPipeGeneration = nullptr; + boundWorkerAckEvent = nullptr; + boundWorkerPipePublishedEvent = nullptr; } bool IsNamedpipeFocusStateOwner(const void *owner) @@ -1771,7 +1792,7 @@ bool EnsureNamedpipeFocusSessionActivated() if (boundWorkerCommitReady) { const ULONGLONG deadline = GetTickCount64() + 150; - while (GetTickCount64() < deadline) + for (;;) { if (IsLocalSessionResetPending()) { @@ -1781,7 +1802,23 @@ bool EnsureNamedpipeFocusSessionActivated() { return true; } - Sleep(1); + const ULONGLONG now = GetTickCount64(); + if (now >= deadline) + { + break; + } + // Block until the IPC worker records the acknowledgement. Sleep(1) + // rounds up to a timer tick (~15.6 ms by default), so every focus + // change paid a tick even though the echo arrives in well under a + // millisecond. The check above runs after every wake-up and the + // worker signals after its release stores, so a wake-up cannot be + // lost; a stale signal only costs one more pass. Like Sleep, this + // wait does not pump messages, so no TSF callback can re-enter here. + if (!boundWorkerAckEvent || + WaitForSingleObject(boundWorkerAckEvent, static_cast(deadline - now)) == WAIT_FAILED) + { + Sleep(1); + } } MarkNamedpipeSessionDirty(); } diff --git a/windows/src/IPC/Ipc.h b/windows/src/IPC/Ipc.h index 399fc026..f097db3a 100644 --- a/windows/src/IPC/Ipc.h +++ b/windows/src/IPC/Ipc.h @@ -25,6 +25,14 @@ void BindNamedpipeFocusState( _In_opt_ std::atomic *localSessionResetToken, _In_opt_ std::atomic *workerCommitReady, _In_opt_ std::atomic *acknowledgedWorkerFocusToken, _In_opt_ std::atomic *workerPipeHandle, _In_opt_ std::atomic *workerPipeGeneration); +// Wake-up events owned by the bound service, so the UI thread and the IPC worker +// thread block instead of polling. workerAckEvent is signalled by the worker when +// it records the expected focus token; workerPipePublishedEvent is signalled when +// a worker pipe handle is published. Either may be null, which falls back to the +// original polling. Only the current owner may bind; UnbindNamedpipeFocusState +// clears them, and the owner must rebind null before closing the handles. +void BindNamedpipeWakeEvents(_In_ const void *owner, _In_opt_ HANDLE workerAckEvent, + _In_opt_ HANDLE workerPipePublishedEvent); void UnbindNamedpipeFocusState(_In_ const void *owner); bool IsNamedpipeFocusStateOwner(_In_ const void *owner); UINT BeginNamedpipeLocalSessionReset();