Wine / スレッド同期 · 動画再生直前 · msync / wineserver

msyncが無いと動画開始が5秒遅れる

Wineのプロセス内同期を有効にし、約5秒の待機を解消する

現象Windowsでは直後に始まる動画が、macOS上のWineでは白いまま約6秒待機する。
原因スレッド同期がwineserverを2312回往復し、待機が白停止の82%を占めていた。
結果msyncを移植すると往復待機が消え、白い保持は一瞬まで短縮した。

Windowsでは一瞬なのにMacでは約5秒止まる

ゲームのシーンが白へフェードする。動画が始まる合図だ。

Windowsではフェードの直後に動画が始まり、保持という間は無い。

同じ操作をWine上のmacOSで行うと、フェードした後の画面が五秒以上白のまま止まり動画はまだ何も映さない。

動画ファイル自体の冒頭にも白い一瞬はあるが、1秒に満たない正しい演出の白だ。止まっているのはその手前で、演出ではない白がもう一段挟まっている。

動画デコードとファイルI/Oを原因候補から外す

動画合成、動画データの展開、出力デバイス列挙を観測し、白停止の原因ではないことを確認した。

合成の一瞬のバグ棄却

映像を重ねる処理が一瞬だけ乱れて白いコマが混じったのではないか。画面を録画して色の変化を追うと、止まっているのは1コマではなく数秒間まるごとで乱れではなく保持だと分かる。

動画データの展開が重い棄却

動画ファイルは150MBを超えるので、白の間にこれを読み込んでいるだけではないかと疑った。ファイルアクセスをそのまま観測すると、白の間に大きな読み込みは一件も無い。動画本体が動き出すのは白が終わった後だった。

出力デバイスの列挙が重い誤読

詳細なログを仕掛けて動きを追った。白の途中に約2秒、何も記録されない空白があった。直後に並ぶ行は音声の出力先候補ばかりで、これを列挙しているから重いのだと考えた。

後で計測の掛け方を変えて確かめ直すと、空白の正体は詳細ログ自体の重さだったという結論に至った。その場所に本物の負荷は無い。計測は、計測している側の重さも記録してしまうことがある。

OSトレースで5秒待ちの発生箇所を測る

詳細ログは重すぎて計測対象を歪めるので、今度は処理を止めないOS標準の生トレースで測り直した。

白の間、ファイルの読み込みは動画にもゲームデータにも無い。候補②はここで完全に消える。

代わりに支配的だったのは、ゲーム内部の複数スレッドが互いにやり取りする小さな読み書きだった。白い窓の約6.3秒のうち5.16秒、82%がこれに費やされている。

回数を数えると2312回。1回あたり平均2.2ミリ秒の応答待ちが、動画が始まる直前まで途切れず続いていた。

ゲームとwineserverの待機を分ける

誰が誰を待たせているかを突き止めるため、ゲーム本体とwineserver(Wineが裏で動かす調整用のプロセス)を同時に観測した。

wineserverは、白の窓のほぼ全体で待機中だった。サンプルの94.7%が次の要求を待つだけの状態で、CPU使用率は4%に満たない。

ゲーム側では複数のスレッドが「解放した」「合図した」を交互に送り合い、そのたびに次のスレッドが応答を待っていた。掛け合いのような動きだ。CPU使用率は32%で、計算というより往復待ちで足踏みしている状態だった。

wineserverは、待たせている側ではなかった。疑うべきは「wineserverが遅い」ことではなく、「毎回wineserverへ聞きに行く作り」だった。

msync欠落で同期がプロセス間通信になる

同期処理のソースを探すと、あるはずの仕組みが見当たらなかった。

プロセス内で完結する高速な同期の実装を探したが、このWineフォークには存在しない。あるのは、必ずwineserverへ問い合わせる経路だけだった。

起動オプションには、msyncを有効にする環境変数WINEMSYNCがすでに渡されている。だがその値を実際に読みに行くコードはどこにも見当たらなかったという事実に行き当たった。渡しているのに、受け取る側が存在しない。

切り替えて確認する。違いは、待ち合わせがwineserverを経由するかどうかだけ。
スレッドA
合図を出したい → wineserverへ要求を送る
wineserver
要求を受け取り、待っているスレッドを起こすよう送り返す
スレッドB
応答が届くまで待つ(1回 約2.2ms)
↓ これを動画開始までに2312回繰り返す
往復の合計 = 5.16秒 → 白いまま止まる 合図を送るだけの操作が、毎回プロセスをまたぐ通信になっていた。
スレッドA
OS標準の、プロセス内で共有できる合図の仕組みを直接使う
スレッドB
同じ仕組みを直接見ているので、要求も応答も無しにそのまま起きる
↓ wineserverを経由しない
往復ゼロ → 保持は一瞬 プロセスをまたぐ通信が要らなくなり、2312回はほぼコストの無い操作になった。

実装したのは、そのmsyncだった。macOS標準のプロセス間共有セマフォ(一枚の整理券のようなもので、それを受け取ったスレッドだけが先に進める仕組み)を使ってプロセス内だけで完結する仕組みだ。これをこのWineフォークへ移植した。

配備して実行すると、白い保持は一瞬に縮んで体感できるほど短くなった。

msync移植後は待機がプロセス内で完結する

観測 A ── 白の82%はスレッド間の待ち

window = 6.33s
thread handoff read = 5.16s (82%)
n = 2312 / avg 2.2ms

白い窓の内訳をそのまま集計すると、大半がスレッド間のやり取りの応答待ちだった。

観測 B ── wineserverは待たせている側ではない

idle samples = 94.7%
CPU使用率 < 4%

wineserverは要求を受けたら即座に返すだけで、詰まりや競合は見られなかった。

観測 C ── msyncの実装が存在しない

msync.c / esync.c = 無し
WINEMSYNCを読むコード = 0件

WINEMSYNCは渡されていたが、それを読むmsyncの実装自体がどこにも無かった。

OSトレースから待機原因を絞り込む

録画で持続時間を確定し、詳細ログの誤りに気づいてから測り方を変えた。二つのプロセスを同時に見てソースへ辿り着いた。

録画で持続時間を確定する

画面を録画して色の変化を追い、1コマの乱れではなく数秒間の保持だという点を先に確定させた。

詳細ログで当たりをつける

内部の動きを詳しく記録するログを仕掛け、白の途中に現れる空白の直後の手がかりから候補を立てた。

測り方を変えて候補を検算する

同じ候補を処理を止めない生トレースでもう一度確かめた。詳細ログの重さ自体が空白を作っていたと分かり、候補は消えた。

生トレースで実処理だけを見る

無計装のファイルシステムトレースで白の窓を集計し、スレッド間の応答待ちが支配的だと確定させた。

二つのプロセスを同時に見る

ゲーム本体とwineserverを同時にサンプリングし、待たせている側と待たされている側を切り分けた。

ソースを読んで欠けている実装を確認する

同期処理のソースを探し、期待していた近道の実装が存在しないことをコードで確認した。

同期処理は実装の有無と実行経路を確認する

詳細ログは対象そのものを歪める

重い計測は「重い場所」ではなく「計測している自分の重さ」を教えてしまうことがある。疑わしい箇所ほど処理を止めない生トレースで検算する。

待たせている側と待たされている側を同時に見る

片方だけを観測していると「遅い」としか分からない。両方を同時に見て初めて、誰が誰を足止めしているかが分かる。

渡したフラグが読まれているか確かめる

起動オプションが効いていると信じる前に、それを読むコードが実在するかをソースで確かめる。渡しているだけで、届いていないことがある。

1回は軽くても、回数を数える

1回あたり数ミリ秒の待ちは誤差に見える。だが千の桁で繰り返されれば、体感できる遅さになる。

修正済みの範囲と一般化しない条件

原因と修正は確認済みだが、結果を未検証の範囲へ広げることはしない。

解決済み
  • プロセス内で完結する同期の仕組みを、このWineフォークへ移植した。
  • 移植後、白い保持は一瞬に縮んで体感できるほど短くなった。
  • 動画を使う複数のタイトルで確認し、副作用は見られなかった。
  • 変更はWine本体側のみ。ゲーム側のファイルには触れていない。
別の成立条件
  • 今回の近道が効くのは、この待ち方の種類に限る。別の種類の待ち合わせでは、依然としてwineserverへの往復が残る可能性がある。
  • 音声側で別に見つかっている遅延は、この記録とは原因が独立している。
  • 対象は動画再生が始まる直前の区間。ゲーム全体のあらゆる待ち合わせを検証したわけではない。