fix: Chromium 系アプリの IME activateServer 時のハングを回避#317
Conversation
Chrome 等 Chromium 系ブラウザで claude.ai / chatgpt.com のような大規模 JS バンドルの ページを開いた直後、azooKey が 6 秒以上フリーズする問題を修正する。 ## 根本原因 1. ページが autofocus → Chrome が NSTextInputContext.activate() を発行 2. macOS HIToolbox が azooKey の activateServer(_:) を同期 XPC で起動 3. activateServer 内で client.attributes(forCharacterIndex:lineHeightRectangle:) を呼び出す 4. Chrome の NSTextInputClient 実装が Renderer へ同期 IPC を発行し pthread_cond_wait で待機 5. Renderer は V8 の 6+ 秒 JS コンパイル中でキュー処理不可 → azooKey もブロック → フリーズ 根本修正は Chromium 側にあり (https://issues.chromium.org/issues/503787240)、 Safari では WebKit 実装が異なるため発生しない。 ## 変更内容 - makeCandidateWindow から inputClient?.attributes(forCharacterIndex:) 呼び出しを削除 - ウィンドウは直後に orderOut されるため origin はユーザーから不可視 - 最初の候補表示時に refreshCandidateWindow() で正しい位置に再配置される - activateServer から client.attributes(forCharacterIndex:) 呼び出しを削除 - 空候補配列を渡すため BaseCandidateViewController.resizeWindowToFitContent は numberOfVisibleRows == 0 で早期 return し cursorLocation は使われない - refreshCandidateWindow() / refreshPredictionWindow() を明示的な hide/orderOut に変更 - これらは composing/selecting 状態で client.attributes(...) を呼ぶ経路があるため activate 中は使わない - 安全性の根拠を保護する回帰テスト ChromiumDeadlockRegressionTests を追加 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Chromium 系アプリ(Chrome など)でページ直後の activateServer(_:) から client.attributes(forCharacterIndex:) を同期呼び出しした際に発生するハング(deadlock)を回避し、IME アクティベーション時のフリーズを防ぐための変更です。
Changes:
makeCandidateWindow/activateServerからclient.attributes(forCharacterIndex:)呼び出しを除去し、Chromium 側の同期待ちに巻き込まれないように変更activateServer中はrefreshCandidateWindow()/refreshPredictionWindow()を避け、明示的にウィンドウを hide /orderOutするよう変更- 「空候補のとき
numberOfVisibleRows == 0で早期 return される」前提を保護する回帰テストを追加
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| azooKeyMac/InputController/azooKeyMacInputController.swift | activate/init 時の client.attributes(...) 同期呼び出しを避け、deadlock 経路を遮断 |
| azooKeyMacTests/ChromiumDeadlockRegressionTests.swift | 空候補時に numberOfVisibleRows == 0 を満たすことを回帰テストで固定 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final class ChromiumDeadlockRegressionTests: XCTestCase { | ||
| /// 空の候補配列で updateCandidatePresentations を呼んだ後、 | ||
| /// numberOfVisibleRows が 0 であることを確認する。 | ||
| /// | ||
| /// これにより、resizeWindowToFitContent の `numberOfVisibleRows == 0` での | ||
| /// 早期 return が継続的に機能することを保護する。 | ||
| func test空配列でupdateCandidatePresentationsを呼ぶとnumberOfVisibleRowsが0になる() { | ||
| let vc = CandidatesViewController() | ||
| _ = vc.view // loadView を強制実行 | ||
| vc.updateCandidatePresentations([], selectionIndex: nil, cursorLocation: .zero) | ||
| XCTAssertEqual( | ||
| vc.numberOfVisibleRows, | ||
| 0, | ||
| "空の候補配列では numberOfVisibleRows は 0 でなければならない" | ||
| ) | ||
| } | ||
|
|
||
| /// 空の候補配列で updateCandidatePresentations を呼んだ後、 | ||
| /// candidates プロパティが空であることを確認する。 | ||
| func test空配列でupdateCandidatePresentationsを呼ぶとcandidatesが空になる() { | ||
| let vc = CandidatesViewController() | ||
| _ = vc.view | ||
| vc.updateCandidatePresentations([], selectionIndex: nil, cursorLocation: .zero) | ||
| XCTAssertTrue(vc.candidates.isEmpty, "空の候補配列を渡した後、candidates は空でなければならない") | ||
| } | ||
|
|
||
| /// 0 より多い候補配列を渡した後に空配列で更新すると、 | ||
| /// numberOfVisibleRows が 0 に戻ることを確認する。 | ||
| func test候補が存在する状態から空配列に更新するとnumberOfVisibleRowsが0になる() { | ||
| let vc = CandidatesViewController() | ||
| _ = vc.view |
There was a problem hiding this comment.
These tests instantiate and drive AppKit view controllers (CandidatesViewController, vc.view, NSTableView.reloadData() etc.). XCTest doesn’t guarantee test methods run on the main thread, and AppKit is not thread-safe, so this can become flaky or crash under parallel testing. Consider marking the test class (or each test method) as @MainActor, or wrapping the body in await MainActor.run { ... } to ensure all UI interactions happen on the main thread.
There was a problem hiding this comment.
対応しました。テストクラスに @MainActor を追加し、すべての AppKit 操作がメインスレッドで実行されることを保証しました。
XCTest はテストメソッドのメインスレッド実行を保証しないため、 AppKit の ViewController を操作するテストが flaky になる可能性がある。 @mainactor を付与してすべての UI 操作をメインスレッドで実行することを保証する。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
問題
Chrome 等 Chromium 系ブラウザで claude.ai / chatgpt.com のような大規模 JS バンドルのページを開いた直後、azooKey がフリーズする問題です。
deadlock の発生フロー:
NSTextInputContext.activate()を発行activateServer(_:)を同期 XPC で起動activateServer内でclient.attributes(forCharacterIndex:lineHeightRectangle:)を呼び出すNSTextInputClient実装が Renderer へ同期 IPC を発行しpthread_cond_waitで待機根本原因は Chromium 側の実装にあります( https://issues.chromium.org/issues/503787240 )。Safari は WebKit の
firstRectForCharacterRange:実装が異なるため発生しません。修正内容
makeCandidateWindowとactivateServerからclient.attributes(forCharacterIndex:lineHeightRectangle:)呼び出しを削除します。以下の理由から機能的影響はありません。makeCandidateWindow: ウィンドウは直後にorderOutされるため、初期 origin はユーザーから不可視。最初の候補表示時にrefreshCandidateWindow()で正しい位置に再配置されます。activateServer: 空の候補配列を渡しているためBaseCandidateViewController.resizeWindowToFitContentはnumberOfVisibleRows == 0で早期 return し、cursorLocationは使われません。また
activateServer内のrefreshCandidateWindow()/refreshPredictionWindow()を明示的な hide/orderOut に置き換えます。これらのメソッドは composing/selecting 状態でclient.attributes(...)を呼ぶ経路があるためです。変更ファイル
azooKeyMac/InputController/azooKeyMacInputController.swiftmakeCandidateWindowからinputClient引数とattributes(forCharacterIndex:)呼び出しを削除activateServerからattributes(forCharacterIndex:)呼び出しを削除refreshCandidateWindow()/refreshPredictionWindow()を明示的なウィンドウ hide に変更azooKeyMacTests/ChromiumDeadlockRegressionTests.swift(新規)動作確認
Refs: https://issues.chromium.org/issues/503787240