閉じるときの順番と GC¶
扱うこと: 画面を閉じるときの手順(インデックス作成中の確認・スレッドの片づけ順)、GC がスレッドを止める問題とその対策。扱わないこと: プロセス・スレッドの一覧・寿命そのもの(プロセスとスレッド)。先に読むページ: プロセスとスレッド。
閉じるときの順番¶
sequenceDiagram
participant U as 利用者
participant G as 画面
participant I as インデクサ
participant S as 検索・バックグラウンド
U->>G: 閉じる
alt インデックス作成中
G->>U: 「インデックスを更新中です。中止して閉じますか?」
U->>G: [中止して閉じる]
G->>I: Stop
G->>G: 閉じるのを保留して「更新を止めています…」<br>待つ間のダイアログ(秒読みつき)を出す<br>closeTimer(0.5 秒ごと)で終わりを待つ
I->>I: 取り込み中のファイルの区切りで止まる<br>本文インデックスを書き出す・Office を終了
I-->>G: 終了
end
G->>S: 検索を取り消す
G->>G: closeTimer・インデックス作成のタイマーを止める
G->>I: IndexingSession.Close
G->>S: 検索の司令(照合のプール)を閉じる
G->>S: バックグラウンド・ネットワークを調べる列に Abandon(止まった仕事を待たない)
G->>G: ミューテックスを解放
G->>G: 画面のスレッドの Dispatcher を止める
G->>U: 閉じる
- 確認の選択肢は[中止して閉じる]と[閉じない]だけである。インデックス作成を続けたまま画面を閉じることはできない。確認している間にインデックス作成が終わっていれば、そのまま閉じる。
- Office が固まってインデクサが戻らないときは、まず監視が制限時間で Office を止める。画面は 60 秒待っても終わらなければ、インデックス作成が起動した Office を PID で止め(
IndexingSession.KillOffice)、さらに 15 秒待つ。それでも終わらなければ、そのまま閉じる(スレッドはIndexingSession.Closeで止める)。 - 止めるのは、インデックス作成が起動して PID を記録した Office(
OfficePids)だけである。PID が別のプロセスに使い回されていないよう、止める前に記録したプロセス名と起動時刻が同じか確かめる(isSameOfficeProcess。起動時刻が読めないものは止めない)。利用者が開いていた Office は止めない。取り込み中に利用者が窓を開いた PowerPoint(UserOfficePidsに印がある、または今見える窓がある)も止めない。 - 止めるのを待っている間にもう一度閉じようとしても、何もしない。
- バックグラウンド・ネットワークを調べる列は、
CloseではなくBackgroundQueue.Abandonで片づける。 届かない共有のTest-Pathなど、OS の呼び出しで戻らない仕事があると、Close(WorkerPool.Cancel→PowerShell.Stop()→Dispose())は止まるまで待ってしまう。Abandonは、終わった仕事は片づけ、終わっていない仕事には止める依頼(BeginStop)だけを出してすぐ戻る。終わっていない仕事のPowerShellとランスペースのプールはDisposeせず、後始末はプロセスの終わりに任せる。WorkerPool.Cancel・BackgroundQueue.Close自体の動き(止まるまで待つ)は変えない。中止した検索がその後にヒットや件数を書かないことが、検索の中止(取り込みの並列化ではなくcontent_index_search.ps1の中止)で前提になっているため。 - 多重起動の鍵(ミューテックス)は、止まった裏の仕事を待たずにすぐ放す。 画面はすぐ消え、前のプロセスは止まった呼び出しが戻った後(OS の呼び出しの分だけ)に自分で終わる。次の起動は、鍵が放たれていれば数秒以内にできる。
- 最後に、画面のスレッドの Dispatcher を止める(
InvokeShutdown)。 止めずにstartGuiから戻ると、PowerShell が終わるときの片づけで、まれに終了コード 5 で終わることがある。例外で抜けるときは止めない(起動の失敗を知らせる画面が同じ Dispatcher を使うため)。二重起動で早く抜ける道筋(exit)も通らず、この直しの対象にしていない。 - 止まった段を後から読めるよう、環境変数
TEBUNKO_CLOSE_TRACEにファイルのパスがあるときだけ、各段の前に段の名前と時刻を 1 行ずつ足して書く(writeCloseTrace -Path <記録先> -Step <段>。shared/core/close_trace.ps1)。環境変数を読むのはgui_main.ps1の起動時の 1 か所だけで、記録先は引数で渡す。段は、Closingのハンドラの先頭・enterIndexingStopFlowの先頭・finallyの各段(indexingSession.Close・searchService.Close・backgroundQueue.Abandon・networkQueue.Abandonなど)・InvokeShutdownの前。書けなくても例外は出さない(記録のせいで閉じる処理を止めない)。環境変数が無ければ何も書かず、動きは変わらない。画面のスモークテストは、場面ごとの作業フォルダのclose_trace.txtにして起動し、失敗の材料に写す。 - 閉じる処理の途中で予期しない例外が起きたら、知らせたうえで、インデックス作成中なら確認を出さずに止める流れに入り、そうでなければそのまま閉じる。
インデックスの削除・名前の変更の途中で閉じるとき¶
削除・名前の変更は裏の列で動く(ネットワークのワークスペース)。途中で閉じると、設定への反映が抜けて、設定とインデックスのフォルダの名前が食い違うため、次のように待つ。
- 1 回目の閉じる操作は閉じずに、「インデックスの変更が終わってから閉じます…」をステータスに出し、終わってから閉じる。複数削除の結果のダイアログは、この間は出さずに閉じる(失敗があればステータスに残す)。
- 待っている間にもう一度閉じようとしたら、[待たずに閉じる]/[終わるまで待つ]の確認を出す。[終わるまで待つ]なら、1. のまま終わりを待つ。確認を出している間に仕事が終わったときは、答えにかかわらず、確認から戻ったときに閉じる。
- [待たずに閉じる]で閉じたときは、仕事は途中で止まる。次の起動で見えるのは次のとおり。
- 名前の変更: フォルダだけが新しい名前に変わり、設定は古い名前のまま、になり得る。一覧には設定の古い名前が出て、そのインデックスは無いものとして扱われる。もう一度同じ名前の変更をすると、旧名のフォルダが無いのでフォルダは移さず、設定と記録だけが新しい名前になる(
IndexCatalog.RenameIndex)。 - 削除: インデックスのフォルダが一部だけ消え、設定には残り得る。もう一度削除すれば、残りを消して設定からも外れる。
ワークスペースの移動の途中で閉じるとき¶
ワークスペースの中身の移動は裏の列で動く(startWorkspaceMove。待つ間のダイアログ ワークスペースを移しています… を出している)。途中で閉じると、中身が移動元と移動先に分かれるため、止めずに待つ。
- 閉じる操作は閉じずに、「ワークスペースを移し終わってから閉じます…」をステータスに出し、待つ間のダイアログの説明を
移し終わると、画面を閉じます。に変える。確認は出さず、待つのをやめる道は無い(移動が終わるまで)。 - 移し終わると、設定は新しいワークスペースになっており、画面は切り替えずに閉じる。次の起動で、移した先のワークスペースが開く。
- 移動が失敗したときは閉じない。警告を出し、今のワークスペースのまま使える(失敗したら
WorkspaceMoverが設定を変えずに戻す)。
GC とメモリ¶
スレッドは同じプロセスのヒープを使うため、インデクサや検索が大量にメモリを使うと、GC で画面のスレッドも止まる。
実測(画面の代わりに 5ms ごとに起きるスレッドを動かし、遅れを測った。ヒープ 300MB、20 秒)では、次のとおりだった。
| 負荷 | 遅れの最大 | 50ms を超えた回数 |
|---|---|---|
| なし | 27.0ms | 0 |
| 同じプロセスのスレッドで本文インデックスの読み書き | 45.0ms | 0 |
| 別のプロセスで同じ負荷 | 18.8ms | 0 |
同じプロセス+SustainedLowLatency |
33.2ms | 0 |
Windows のタイマーの刻み(約 15.6ms)を考えると、どれも画面の操作で気づく止まりではない。そのうえで、次を守る。
- 起動時に
GCSettings.LatencyModeをSustainedLowLatencyにする(重い全体の GC を後回しにする)。 - 読んだ内容のキャッシュに予算(文字数)を決め、超えたら使われていないものから追い出す。追い出しは検索の司令のスレッドで、検索の後に行う(
trimTsvTextCache)。 - キャッシュの各内容には、最後に使った世代を付ける。世代は検索 1 回ごとに 1 つ進める。
- 予算の 90% を超えていたら、今の世代(いま終わった検索)で使わなかったものを、古い世代から追い出す。今の世代で使ったものは残す(毎回同じ本文インデックスのファイルを順に読むため、使った順だけで追い出すと、予算より大きいインデックスでは何も残らない)。
- 画面のスレッドで大きな文字列を作らない。プレビューの読み込み(
readContentIndexLines)はバックグラウンドのスレッドで行い、表示する分だけを受け取る。 - 検索の結果は、時間で区切って少しずつ画面に移す(
pumpSearch)。
実装を変えたときは、画面の心拍(画面のスレッドの DispatcherTimer の遅れ)を、インデックス作成と検索を同時に動かして測る。100ms を超える止まりが出るようなら、インデクサだけを別のプロセスに戻すことを検討する。