Skip to content

fix: Chromium 系アプリの IME activateServer 時のハングを回避#317

Merged
ensan-hcl merged 2 commits intoazooKey:mainfrom
mtane0412:fix/chromium-ime-deadlock-workaround-upstream
Apr 21, 2026
Merged

fix: Chromium 系アプリの IME activateServer 時のハングを回避#317
ensan-hcl merged 2 commits intoazooKey:mainfrom
mtane0412:fix/chromium-ime-deadlock-workaround-upstream

Conversation

@mtane0412
Copy link
Copy Markdown
Contributor

@mtane0412 mtane0412 commented Apr 19, 2026

問題

Chrome 等 Chromium 系ブラウザで claude.ai / chatgpt.com のような大規模 JS バンドルのページを開いた直後、azooKey がフリーズする問題です。

deadlock の発生フロー:

  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 の JS コンパイル中(6 秒以上)でキュー処理不可
  6. azooKey は Chrome の応答待ちでブロック → UI フリーズ

根本原因は Chromium 側の実装にあります( https://issues.chromium.org/issues/503787240 )。Safari は WebKit の firstRectForCharacterRange: 実装が異なるため発生しません。

修正内容

makeCandidateWindowactivateServer から client.attributes(forCharacterIndex:lineHeightRectangle:) 呼び出しを削除します。以下の理由から機能的影響はありません。

  • makeCandidateWindow: ウィンドウは直後に orderOut されるため、初期 origin はユーザーから不可視。最初の候補表示時に refreshCandidateWindow() で正しい位置に再配置されます。
  • activateServer: 空の候補配列を渡しているため BaseCandidateViewController.resizeWindowToFitContentnumberOfVisibleRows == 0 で早期 return し、cursorLocation は使われません。

また activateServer 内の refreshCandidateWindow() / refreshPredictionWindow() を明示的な hide/orderOut に置き換えます。これらのメソッドは composing/selecting 状態で client.attributes(...) を呼ぶ経路があるためです。

変更ファイル

  • azooKeyMac/InputController/azooKeyMacInputController.swift
    • makeCandidateWindow から inputClient 引数と attributes(forCharacterIndex:) 呼び出しを削除
    • activateServer から attributes(forCharacterIndex:) 呼び出しを削除
    • refreshCandidateWindow() / refreshPredictionWindow() を明示的なウィンドウ hide に変更
  • azooKeyMacTests/ChromiumDeadlockRegressionTests.swift(新規)
    • 安全性の根拠(空候補配列では cursorLocation が使われない)を保護する回帰テストを追加

動作確認

  • Chrome で https://claude.ai および https://chatgpt.com を開いてもフリーズしなくなることを確認
  • TextEdit / Safari / VSCode 等で日本語入力時に候補ウィンドウが正しいカーソル位置に表示されることを確認

Refs: https://issues.chromium.org/issues/503787240

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>
Copilot AI review requested due to automatic review settings April 19, 2026 11:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to +49
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
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

対応しました。テストクラスに @MainActor を追加し、すべての AppKit 操作がメインスレッドで実行されることを保証しました。

XCTest はテストメソッドのメインスレッド実行を保証しないため、
AppKit の ViewController を操作するテストが flaky になる可能性がある。
@mainactor を付与してすべての UI 操作をメインスレッドで実行することを保証する。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@ensan-hcl ensan-hcl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原理的には問題ないと思うのでLGTMです!

@ensan-hcl ensan-hcl merged commit 5b36205 into azooKey:main Apr 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants