FFS Discord に参加しませんか? — サーバー起動・音声・イベントの連絡はこちらで 参加する →

DCS World Dedicated Server Community

Known Issues & Fixes

サーバ運用中に発見された既知の問題と修正方法をまとめています。

[Fix] Olympus 有効化後、無人でも約 2.5 時間でサーバ FPS が 28 まで崩落する — Wine ヒープの空きリスト線形走査 (LFH 未活性化) と ntdll.dll パッチ

DCS Dedicated Server DCS Olympus v2.0.5 Wine 11.0 / ntdll heap cpprestsdk Performance 2026-09-04

Environment

  • DCS World Dedicated Server 2.9.29.27468 (MT)、Linux (Ubuntu 24.04 / kernel 7.0) + WineHQ stable 11.0 (bookworm) + Docker
  • DCS Olympus v2.0.5 — backend (olympus.dll / core.dll / luatools.dll / cpprest_2_10.dll) は DCS プロセス内で動作
  • CPU: Core Ultra 7 265K、シムスレッドは P-core (CPU 0-7) に固定済み (前記事参照)
  • ミッション: 常設フリーフライト (ユニット 777)

Symptom

DCS を起動すると 117 FPS / プロセス CPU 42% から始まり、プレイヤーが 0 人でもシムスレッドの CPU が 1 時間あたり約 +25 pt ずつ増え続け、約 2.5〜3 時間で 1 コアを振り切って FPS 28 に張り付く。再起動すると元に戻り、また同じ勾配で落ちる。人数、離陸、SAM、ミッション内容のいずれとも無関係。

# 9/4 16:01 起動の run (終始 0 人)。プロセス CPU% / FPS
経過   10分   30分   60分   90分  120分  150分  180分  240分
CPU    42     51     59     72     81     98    108    108
FPS   116    107     95     73     64     37     29     29

RSS も 7.1 GB → 8.5 GB と単調に増える。前記事 (E-core 固定) の修正でベースは下がったが、この勾配は変わらなかった。

いつから起きているか

run ごとに「起動からの経過時間 × プロセス CPU%」を並べると、境目は 1 本だけだった。

# server1、run 開始からの経過時間別 プロセス CPU% (人数)
run 開始            10分    60分   120分   180分   360分
08-31 04:07         38/0    39/0    39/0    40/0    41/0    ← 旧 DCS、Olympus なし
09-02 04:19         33/0    34/0    35/0    35/0    38/0
09-03 04:14         43/0    43/0    44/0    44/0    47/0    ← DCS 2.9.29 更新後、Olympus なし: 平坦
09-03 17:23         52/0    69/0    94/0   108/0           ← server1 で Olympus 有効化: 以後すべての run で再現
09-04 04:18         45/0    65/0   100/2   108/0
09-04 16:01         44/0    65/0    93/0   108/1

DCS 本体の更新 (9/3 01:12) 後も Olympus 無効の run は 6 時間平坦なので、DCS 更新は無関係。Olympus を server1 で有効化した run から例外なく発生している。

Method (壊さずに中を測る)

「Olympus を外して直ったら Olympus が悪い」では何が起きているか分からないので、飽和状態のプロセスを止めずにプロファイルした。Wine 配下の PE モジュールはシンボルが取れないため、DSO (どの DLL か) 単位ロード済み PE の実アドレス範囲で帰属させる。

# 1. シムスレッド (main tid) を 499 Hz でサンプリング、DLL 単位で集計
perf record -F 499 -t <main_tid> -- sleep 20
perf report --sort dso,sym --stdio

# 2. 呼び出し元は LBR (Last Branch Record) で取る。PE の .pdata は perf が読めないので
#    フレームポインタ/unwind に頼らない LBR が唯一の手段
perf record -F 499 -t <main_tid> --call-graph lbr -- sleep 10

# 3. [JIT] 扱いになる anonymous mapping を PE モジュールに対応付ける:
#    /proc/<pid>/maps の各領域先頭から 'MZ' → PE ヘッダ → export 名を読む (同一 PID ns から /proc/<pid>/mem)

# 4. ホットループ内のレジスタをサンプリングして「要求サイズ」を直接読む
perf record -F 997 -t <main_tid> --user-regs=R13,R12 -- sleep 6
perf script -F ip,uregs

Findings

CPU 時間の 8 割が Wine の ntdll.dll の 1 つのループ
    85.40%  ntdll.dll          ← うち 77% が ntdll.dll+0x4f500 の 5 命令
      9.18%  [JIT]              ← DCS 本体 (World/Scripting/edCore/lua 等、anonymous mapping)
      3.09%  ntdll.so
      0.56%  ucrtbase.dll

逆アセンブルすると heap_allocate_block の中で空きリストを mov rbx,[rbx] で辿り、各ブロックのサイズを要求サイズ (r13) と比べて「足りなければ次へ」を繰り返す first-fit の走査だった。

17004f500:  movzx  edx,BYTE PTR [rbx-0x1]     ; block flags
17004f504:  cmp    dl,0x3
17004f50c:  movzx  eax,WORD PTR [rbx-0x8]     ; block size
17004f515:  shl    eax,0x4
17004f518:  cmp    rax,r13                    ; size >= requested ?
17004f51b:  jae    found
17004f521:  mov    rbx,QWORD PTR [rbx]        ; next free block
17004f529:  cmp    rbx,r8                     ; list end ?
17004f52c:  jne    17004f500
呼び出し元は Olympus の JSON 構築
World.dll → Scripting.dll → edCore.dll → lua.dll        (DCS がミッション Lua を実行)
  → olympus.dll → core.dll → luatools.dll (再帰 ×7)     (Lua テーブル → JSON 変換)
  → cpprest_2_10.dll (+0x8d81a: 71%)                     (json::value の構築)
  → ucrtbase.dll!malloc → ntdll!RtlAllocateHeap → heap_allocate_block の空きリスト走査

OlympusCommand.luasetUnitsData が 50 ms 毎に 50 ユニット分のテーブルを DLL に渡し、DLL 側が cpprestsdk の JSON オブジェクトに変換して捨てる。毎秒約 1,000 回の割り当てと解放である。

要求サイズは 100% が 1,520 byte
# ホットループ内で採取した 5,160 サンプルの要求サイズ (r13) とヒープハンドル (r12)
block_size 0x5f0 (1.5 KB): 5160 samples (100.0%)
heap handle: 0x7ffffe220000 のみ (ucrtbase の CRT ヒープ)

Root Cause (wine-11.0 dlls/ntdll/heap.c)

Wine のヒープには LFH (Low Fragmentation Heap) フロントエンドがあり、サイズ帯 (bin) ごとに自動で有効化される。その条件が bin_try_enable() にある:

if (bin == heap->bins && alloc > 0x10) enable = TRUE;
else if (bin - heap->bins < 0x30 && alloc > 0x800) enable = TRUE;          /* 小さい bin: 累計 2048 回で有効化 */
else if (bin - heap->bins < 0x30 && alloc - freed > 0x10) enable = TRUE;
else if (alloc - freed > 0x400000 / block_size) enable = TRUE;             /* それ以外: 生存ブロック数のみ */

第一仮説 (誤り): 1,520 byte は bin 0x37 (≥ 0x30) で、LFH 化には「生存ブロック 2,730 個超」が必要なため、確保→即解放の Olympus ではこのサイズが永久に LFH 化されない、と読んだ。この規則を緩めた ntdll.dll (パッチ 1) を当てて検証したが、走査は消えず CPU も同じ勾配で上昇した (10 分 40% → 34 分 55%)。プロセスメモリから struct heap を直接読むと bin 0x37 は enabled = 1 で、標準経路のカウンタも増えていなかった。

確定した機構: LBR が示す直接の呼び出し元 RtlAllocateHeap+0x725 を逆アセンブルすると、そこは通常の割り当てではなく LFH のグループ確保 (group_allocate、インライン化) だった。

17001b5e0:  mov    rax,r12          ; r12 = 0x30 (bin 2 = 48 byte ブロック)
17001b5e3:  shl    rax,0x5
17001b5e7:  sub    rax,r12          ; rax = 31 * 0x30  (GROUP_BLOCK_COUNT = 31)
17001b5ea:  lea    r8,[r15+rax*1+0x27]
17001b5f6:  and    r8,0xfffffffffffffff0   ; = 0x5f0 = グループブロックのサイズ
  ...
17001b655:  call   heap_allocate_block     ; ← 走査の 100% がここから
  • 0x5f0 = 1,520 byte は「48 byte の JSON ノード 31 個 + ヘッダ」の LFH グループのサイズ。cpprest は json::value のノードを 48 byte 単位で大量に作る。
  • 空になった LFH グループの行き先は、スレッドの affinity スロット (1 個) → bin の共有リスト (32 個まで) → それ以上は group_release()標準ヒープへ解放。Olympus は 1 サイクルにノードを数千個作って捨てるので、毎サイクル 33 個を超えるグループが空になり、余りが解放される。次のサイクルで group_allocate()heap_allocate_block(0x5f0) で再確保する。これが毎秒数百〜千回。
  • LFH が有効な間、標準経路を通る要求は事実上このグループ確保だけになる。first-fit の分割で出る「0x5f0 に届かない端数 (0x500〜0x5ef)」は、そのサイズ帯の要求が LFH に吸われて標準経路に来ないため誰にも消費されず同じリストに溜まり続け、グループ確保のたびに全部スキップされる。断片数が稼働時間に比例するので走査時間も比例して伸びる。
  • Windows の LFH はグループをこの規模で標準ヒープに戻さないため実機では起きない。Wine 固有で、Olympus 側から見れば「Windows では正常」なコードである。

確認して外れた仮説

  • プレイヤー / SAM / ミッション: 0 人の run で同じ勾配。
  • DCS 2.9.29 更新: 更新後・Olympus 無効の run は 6 時間平坦。
  • Olympus のリストが肥大 (ユニット/武器/ログ): backend API の /olympus/units は 62 KB、/weapons は 4 件で一定。Lua 側の Olympus.units も死亡ユニットを除去している。
  • Lua からのヒープ操作で LFH を強制有効化する実験: 1,520 byte 級の文字列を 4,000 個保持させても変化なし。DCS の Lua VM は独自アロケータで、cpprest と同じヒープに乗らない (lua.dll が realloc を import していても使われていない)。
  • split lock / wineserver / メモリ逼迫: いずれも数 % 以下。

Fix

1. ntdll.dll: 空になった LFH グループを標準ヒープに戻さない (本命)
--- a/dlls/ntdll/heap.c
+++ b/dlls/ntdll/heap.c
@@ heap_release_bin_group()
-    if (RtlQueryDepthSList( &bin->groups ) <= ARRAY_SIZE(affinity_mapping))   /* 32 */
+    if (RtlQueryDepthSList( &bin->groups ) <= 0x1000)
     {
         RtlInterlockedPushEntrySList( &bin->groups, &group->entry );
         return STATUS_SUCCESS;
     }
     return group_release( heap, flags, bin, group );   /* ← ここに落ちなくなる */

@@ bin_try_enable()   (補助: 全 bin を割り当て頻度で LFH 化。単独では効かなかった)
-    else if (bin - heap->bins < 0x30 && alloc > 0x800) enable = TRUE;
+    else if (alloc > 0x800) enable = TRUE;

bin あたり最大 0x1000 個の空グループを保持する (bin 2 なら 1,520 byte × 4,096 = 6 MB が上限)。グループが標準ヒープに戻らなくなるので group_allocate() の走査そのものが発生せず、端数も生まれない。wine-11.0 のソースに当てて mingw で PE 版 ntdll.dll だけを再ビルドし (configure 7 秒、make 4 秒)、/opt/wine-stable/lib/wine/x86_64-windows/ntdll.dll を差し替える。unix 側 ntdll.so は stock のまま (同一ソース版)。差し替えは同ディレクトリにコピーしてから mv -f (rename) で行い、実行中プロセスの旧マッピングを壊さない。

事前検証: 本番と同じイメージの使い捨てコンテナで、1 サイクルに 48 byte ×4,000 + 24 byte ×2,000 + 1,512 byte ×50 + 小ブロックを確保→解放する負荷を 2 万サイクル実行し、wineboot・実行とも正常、stock 230 µs/サイクル → パッチ版 205 µs/サイクル (−11%)。合成負荷では stock 側の「時間比例の劣化」までは再現できず (本番の断片化には多スレッド + 数十万サイクルが要る)、機構の確認は本番の perf で行った。

2. 暫定緩和 (パッチが使えない場合)

OlympusCommand.luasetUnitsData の周期 return time + 0.050.25 にすると割り当て回数が 1/5 になり、飽和まで約 12 時間に伸びる (地図の全ユニット更新周期は 0.8 秒→4 秒)。断片化そのものは止まらない。

Status / Verification

  • 2026-09-04 23:12 JST: パッチ 1 (LFH 化規則の緩和) のみで server1 を再起動 → 34 分で CPU 40% → 55%、走査も残存。効かず。この検証で呼び出し元がグループ確保だと判明。
  • 2026-09-04 23:51 JST: パッチ 1+2 を適用し、Olympus の周期は元の 0.05 秒のまま再起動。4 分後の perf でシムスレッド中の ntdll.dll は 9.6% (前 run 同時点 40%、飽和時 85%)、heap_allocate_block の走査は検出されず、DCS 本体コードが 56%。Main の CPU は 26% (前 run 同時点 37〜40%)。
  • 2026-09-05 01:47 JST 追記: 適用後 2 時間、無人で完全に平坦。従来は 120 分で 100% に達していた。
  • 2026-09-05 02:46 JST 追記: 適用後 175 分 (0 人) で CPU 38.2% / 117.7 FPS。3 時間判定も合格。RSS は 6.8 → 8.3 GB (ミッション側の増分と推定、12 時間の定期再起動内で従来実績 11.4 GB の範囲)。有人時間帯の挙動は引き続き記録する。
  • # 再起動 4 分後、シムスレッドの内訳 (perf, DSO 別)
                  前 run (パッチ 1 のみ)   今回 (パッチ 1+2)
    ntdll.dll              40.8%              9.6%   ← heap_allocate_block が消滅
    DCS 本体 [JIT]         38.5%             55.6%
    ntdll.so (syscall)     11.1%             20.0%
  • Wine 上流への報告: 「1 サイクルに同サイズの割り当てを数千個作って捨てる負荷で、空 LFH グループが 32 個超で標準ヒープに戻され、再確保の first-fit 走査が分割端数の蓄積で時間比例に肥大する」。再現データ (perf/LBR/レジスタ/逆アセンブル) はこの記事のとおり。
  • Olympus 上流には「Windows では起きない」ことも含めて情報提供する。JSON オブジェクトの使い捨てを減らせば Wine でも軽くなる。

[Fix] 日によってサーバ FPS が 5〜30 まで落ちる — DCSServerBot auto_affinity による E-core 固定と、SAM レーダー制御スクリプトの A/B 結果

DCS Dedicated Server DCSServerBot Linux / Wine Hybrid CPU (P-core / E-core) Performance 2026-09-03

Environment

  • DCS World Dedicated Server 2.9.29.27468 (MT ビルド、--server --norender)
  • Linux (Ubuntu 24.04 / kernel 7.0) + Wine 11.0 stable (WineHQ bookworm パッケージ) + Docker
  • CPU: Intel Core Ultra 7 265K — P-core 8 基 (CPU 0-7、最大 5.5 GHz) + E-core 12 基 (CPU 8-19、最大 4.6 GHz)、SMT なし
  • プロセス管理: DCSServerBot 3.0.4 (Linux native、Wine 配下の DCS_server.exesudo -u <user> wine で起動)
  • ミッション: 常設フリーフライト (ユニット 777 / グループ 473、うちクライアントスロット 309、SAM 87 サイト・EWR 20)

Symptom

プレイヤーが離陸するとシムスレッドが 1 コアを振り切り (プロセス CPU 105〜118 %)、サーバ FPS が平常 110 から 5〜30 まで落ちる。降りると即座に復帰する。

人数依存は非線形で、16 人で 52 FPS の日もあれば、1 人で 20 FPS の日もある。8/30 には 4〜5 人で 6.6 FPS が 1 時間以上続いた。RAM / I/O / スワップは動いておらず、純粋な CPU 律速。

補足: DCS 2.9.x の専用サーバは既にマルチスレッド (MT) ビルドだが、シミュレーション本体は 1 スレッドで回る。MT 化で分散されるのは描画系が中心で、--norender のサーバでは恩恵が小さい。「マルチコアで動かす」ことでは解決しない種類の問題である。

Method

  • DCSServerBot Monitoring が 1 分毎に書く serverstats (FPS / プロセス CPU / 人数) 45 日分を、ミッション名と人数で層別集計
  • statistics (誰がどの機体でいつ乗っていたか) と JOIN し、低 FPS 区間と機体・人物の相関を確認
  • シムスレッドの許可 CPU (/proc/<pid>/task/*/statusCpus_allowed_list) と、実行中コア (ps -o psr) を 1 秒毎 60 回サンプリング
  • DCSServerBot の core/process/processmanager.pycore/process/linux/cpu.py を読解

Finding 1: シムスレッドが E-core に固定されていた

DCSServerBot 3.0 系の EXPERIMENTAL auto_affinity (2026-01 追加の ProcessManager) が、起動する実行ファイルに 1〜2 物理コアの CPU affinity を自動で割り当てる。Linux 実装はトポロジを sysfs の physical_package_id (= 全コア 0) で分類し、Efficiency Class を常に 0 として扱うため、P-core と E-core を区別できない

affinity は Wine を起動するランチャープロセスに対して設定され、exec を経て DCS のメインスレッド (= シムスレッド) がそれを継承する。DCS のワーカースレッドは自分でマスクを全コアに戻すため、シムスレッドだけが 1〜2 コアに閉じ込められる。

# 当日の実測 (無人、ミッション稼働中)
Cpus_allowed_list (main thread) : 6,8        ← P-core 1 基 + E-core 1 基
実行コア 60 秒サンプリング       : cpu8 = 55 回 / cpu6 = 5 回   ← 92 % の時間を E-core で過ごす
その他 66 スレッド               : 0-19 (制限なし)

起動後の再配分 (2 秒毎の "cooperative" パス) はランチャープロセスにしか届かないため (DCS プロセスは別 UID で EPERM)、起動時に引いたコアが 12 時間の稼働中ずっと固定される。P-core を引いた日は速く、E-core を引いた日は同じ人数でも遅い。これが「日によって性能が違う」観測の説明になる。

なお nodes.yaml で auto_affinity を有効化していないにもかかわらず動作していた。ProcessManager がシングルトンで、設定を渡す前に既定値 (auto_affinity=True) で初期化されているためと推定している (上流側の挙動)。

Finding 2: SAM レーダー制御スクリプトの A/B 結果

ミッション同梱の SAM_RadarOparationLogic (5 秒周期で全グループを走査し、EWR の探知に応じて SAM のレーダー発振を on/off する Lua) が容疑者だったため、8/20 からスクリプトを no-op スタブに置換した版 (B) を稼働させて比較した。B では全 SAM のレーダーが常時 ON になる。

# server1、45 日分、RUNNING 中のみ
ミッション            状態        平均FPS   5%tile   FPS<50    FPS<25   プロセスCPU
A: 原本 (Lua 有効)    無人        110.3     103.3     0.0 %     0.0 %     36.9 %
A: 原本 (Lua 有効)    飛行中      100.2      57.5     3.1 %     0.8 %     53.3 %
B: スタブ (全 ON)     無人         94.2      67.2     0.7 %     0.3 %     39.1 %
B: スタブ (全 ON)     飛行中       76.7      31.7    12.1 %     2.4 %     54.5 %

# 人数別 平均 FPS (A / B)
1 人: 106 / 85   2 人: 101 / 83   4 人: 97 / 74   5 人: 91 / 65   8 人: 72 / 40

結論: コストは Lua ではなく、レーダーが ON になった後のエンジン側レーダーシミュレーションにある。スクリプトは「誰も飛んでいなければ全レーダー OFF、探知圏に入ったものだけ ON」にすることで負荷を下げており、外すと悪化する。SAM 87 / EWR 20 という密度そのものが根本原因で、離陸したプレイヤーが探知圏に入るほど重くなる (A-10 / Su-25T / 攻撃機系の低高度飛行で低 FPS が多いことと整合)。

Not the cause (確認済みの容疑者)

  • Wine の同期オーバーヘッド (wineserver): wineserver の CPU 時間はシムスレッドの 5〜8 % 程度。ntsync (Linux 6.14+ のカーネル同期プリミティブ) は Wine 11.0 が対応しているが、WineHQ の bookworm 向けパッケージは ntsync 非対応でビルドされているため、使うには Wine 自体の再ビルドが必要。効果見込みが小さいので今回は見送り。
  • 同一ホストの他サーバインスタンス: 他インスタンス稼働中と停止中で server1 の飛行中 FPS に差なし (90.6 vs 89.2)。
  • メモリ / スワップ: 低 FPS 区間の PSI (memory / io) は 0。
  • プロセス稼働時間: 無人時 FPS が 12 時間で 106 → 96 に逓減する別事象は存在するが (下記 [Investigation] 記事参照)、本件の 5〜30 FPS への崩落は稼働直後にも起きる。

Fix

1. DCS のシムスレッドを P-core に明示固定 (nodes.yaml)

インスタンス毎に affinity を明示すると、DCSServerBot はその値をそのまま適用し、auto_affinity の管理対象から外す。Core Ultra 7 265K の P-core は CPU 0-7。

# nodes.yaml (各 node の instances.<name> 配下)
      affinity: 0,1,2,3,4,5,6,7

反映には bot コンテナの再起動が必要 (= DCS 再起動)。再起動後、起動直後の DCS 全プロセスで Cpus_allowed_list: 0-7 を確認。dcs.log の Created boot pool: n:8 も 8 コア認識に変わる (以前は n:20)。

稼働中のプロセスに即時適用するには、DCS と同じ UID で全スレッドを対象にする (root でも UID が違うと EPERM):

sudo -u <dcs-user> taskset -a -cp 0-7 <DCS_server.exe の pid>

cgroup の cpuset (docker の --cpuset-cpus) で縛る案は採用しなかった。auto_affinity が cpuset の外側のコアだけを選んだ場合に sched_setaffinityEINVAL となり、DCS 起動処理が例外で失敗しうるため。

2. ミッションを原本 (SAM レーダー制御 Lua 有効) に戻す

A/B の B 側は原本より一貫して悪いため、原本に戻した。無人時 (0 人) に serverSettings.luacurrent / listStartIndex を原本に向けて DCS を再起動。

Result

# 同一ミッション (B)、ほぼ同一負荷 (0〜1 人) での即時比較 — E-core → P-core
                      人数   FPS     プロセスCPU
修正前 (cpu8 に固定)   0.8    107.3   56.5 %
修正後 (0-7)           1.0    112.6   49.5 %     ← 同じ仕事を約 12 % 少ない CPU 時間で処理

# 原本ミッション + P-core 固定で再起動後 (1 人)
                              116.7    39.1 %    ← 過去の A 側平常値 (110 FPS / 37 %) と同水準

無人時の FPS は上限 (約 110〜118) に張り付くため、効果はまずプロセス CPU % に現れる。負荷時の FPS 改善幅は今後の serverstats で追跡する (E-core と P-core の単スレッド性能差は概ね 25〜35 %)。

Status

  • 2026-09-04 追記: 本記事の Result で「修正後 40〜78% の上昇はプレイヤー増加分」と書いたのは誤りで、Olympus 由来の別事象 (ヒープの空きリスト走査が稼働時間に比例して伸びる) だった。上の記事「Olympus 有効化後、無人でも約 2.5 時間でサーバ FPS が 28 まで崩落する」を参照。本記事の E-core 固定と A/B の結論自体は変わらない。
  • P-core 固定と原本ミッションへの切り戻しは 2026-09-03 に適用済み。
  • 効果検証: 今後 2〜4 週間の「FPS < 50 が 5 分以上続くエピソード数 / 100 時間」を、適用前の A 側基準値 3.79 件 / 100h と比較する。
  • 根本対策として、ミッション側の SAM / EWR 密度 (レーダー同時発振数) の見直しを検討中。
  • DCSServerBot の Linux 向け auto_affinity が P/E コアを区別しない件は上流へ報告予定。ハイブリッド CPU の Linux ホストで DCSServerBot を使っている場合は、affinity を明示することを推奨する。

[Investigation] 全プレイヤーが同時に一瞬フリーズする — シムスレッドのバースト肥大とプロセス稼働時間の関係

DCS Dedicated Server Multiplayer Performance 2026-08

Environment

  • DCS World Dedicated Server (Linux / Wine, Docker 上で運用)
  • DCSServerBot によるプロセス管理、12 時間間隔の定期再起動
  • ミッション: 大規模常設マップ (AI ユニット約 580、クライアントスロット約 300)
  • 観測対象人数: 1〜15 名

Symptom

プレイヤーから「その場にいた全員が同時にすっ飛ぶ」という報告。数百ミリ秒の瞬間的フリーズで、不規則に繰り返される。

別症状として、ミッション稼働の後半になるほど全体的に重くなる (ラバーバンド感) 現象も併発する。両者は同一原因の別側面であることが後に判明した。

Why it is hard to detect

通常の監視では、この現象は捉えにくい。切り分けに時間を要した理由:

  • サーバ FPS は 1 分平均のため、300 ms の停止は平均値にほとんど現れない。平均 FPS が健全な区間でも停止は起きていた。
  • NIC の合計バイトカウンタでは判別できない。同一インターフェースにテレメトリ export や DB 通信が相乗りしているため、ゲーム状態の送信が全員に対して止まっても合計値は途切れない。
    実測では、全クライアントが 300 ms 沈黙した時点でも、インターフェース合計の最大途切れは 25 ms だった。
  • プロセス全体の CPU 使用率 (1 秒平均) も、300 ms のフルコア・バーストを埋めてしまう。

Method

以下の 3 点を同時計測することで初めて捕捉できた。

1. シムスレッドをナノ秒精度で 200 Hz サンプリング

jiffy 単位の CPU 時間 (/proc/<pid>/stat) では量子化ノイズにより偽の周期が現れることがある。schedstat を用いる。

# field 1 = 累積 on-CPU 時間 (ns), field 2 = ランキュー待ち時間 (ns)
cat /proc/<pid>/task/<main_tid>/schedstat
2. クライアント別の送信途切れを検出

合計値では見えないため、ゲームポートから出る UDP のみを BPF で抽出し、宛先ごとに 沈黙時間を追跡する。ペイロードは読まない。

# BPF: IPv4 && UDP && src port == <game port>
# AF_PACKET raw socket + SO_ATTACH_FILTER
# 宛先 IP ごとに last-seen を保持し、閾値を超えた沈黙を記録する
#
# 判定: 複数クライアントの沈黙が同時に始まり同時に終わる → サーバ側
#       1 クライアントのみ                              → その回線側
3. 停止「発生中」のスレッド状態

停止を検知した瞬間に全スレッドの状態と wchan を取得する。事後の取得では判定できない。

# R  = 実行中        → 計算で詰まっている
# D  = 割り込み不可  → I/O 待ち
# S + wchan          → 何を待っているかが名前で判る

Findings

停止はサーバ側で、全員同時に起きる

2 クライアント同時の例。開始が 13 ms 以内、復帰が 1 ms 以内で揃っている。個々の回線起因では説明できない挙動である。

client A  silent  T+0.295 .. T+0.598   (303 ms)
client B  silent  T+0.308 .. T+0.597   (289 ms)

9 名接続時のサンプルでは、9 名全員が同時に沈黙していることを確認した。

停止中、シムスレッドは「計算中」

捕捉できた停止のスレッド状態はいずれも R (実行中)。同時刻のメジャーフォルトは 0。I/O 待ちではなく計算による停止である。捕捉した停止は 7/7 が同一秒内の CPU バーストと一致した。

バーストはプロセス稼働時間とともに肥大する

人数をほぼ一定 (0〜2 名) に保った状態での、シムスレッドのバースト計測:

プロセス稼働   バースト長(中央値)   デューティ比
  +0.3 h            56 ms            0.3 %
  +2.3 h            46 ms            0.9 %
  +4.3 h            46 ms            3.6 %
  +6.3 h            50 ms            6.8 %
  +8.3 h            65 ms            9.1 %
 +11.3 h            90 ms           12.7 %

バースト頻度は約 1.5 回/秒で頭打ちになり、1 回あたりの長さが伸び続ける。一定周期のジョブが、増え続けるデータを走査している場合の挙動と一致する。

停止の発生率には明確な閾値がある

プレイヤーが接続していた時間だけを分母に取った発生率:

ミッション経過   露出時間     停止回数   発生率
  2- 4 h         65.8 分         0      0.0 /時
  4- 6 h         92.2 分         0      0.0 /時
  6- 8 h         43.3 分         0      0.0 /時
  8-10 h         96.6 分         3      1.9 /時
 10-12 h         56.5 分         3      3.2 /時

稼働 8 時間未満では、計 201 プレイヤー分を通じて発生が記録されていない。

FPS 低下も同じ原因の別側面

3 週間分の集計 (プレイヤー 3〜5 名時)。60 FPS を割った時間:

ミッション経過      平均 FPS    60FPS 未満の時間
 新鮮   (< 4h)      114.9      0 分 / 459 分
 中間   (4-8h)      102.6      0 分 / 595 分
 古い   (8h+)        86.8    137 分 / 794 分  (最低 7.2 FPS)

Root Cause Isolation

蓄積がミッションの Lua 状態にあるのか、DCS プロセス側にあるのかを切り分けるため、プロセスを維持したままミッションだけ作り直して比較した (無人時に実施)。

                          PID       デューティ比
ミッション +4.2h          同一         0.0524
ミッションのみ再起動直後   同一         0.0642   ← リセットされない
(参考)プロセスごと再起動  新規    0.122 → 0.003   ← リセットされる

ミッションを作り直しても蓄積は解消しない。原因がミッションスクリプト (MIST / CTLD 等) ではなく DCS サーバプロセス側にあることを示す。スクリプト側の最適化では改善しない。

プロセス再起動でのみリセットされることは、2 回の定期再起動で独立に再現している (デューティ比 0.122→0.003 および 0.127→0.001)。

Mitigation

プロセス側の蓄積そのものには手を入れられないため、再起動間隔の短縮で対処する。

要件は「混雑する時間帯がプロセス稼働 8 時間を超えないこと」。12 時間周期では混雑帯を 8 時間以内に収められないため、8 時間周期 (1 日 3 回) へ変更し、混雑帯の直前に再起動が来るよう時刻を配置する。

再起動時刻は各コミュニティの在室分布から決めるべきで、DCSServerBot 利用者は scheduler プラグインの action.times にリストで複数指定できる。

Status

発生機構は確認済み。緩和策は未適用で、効果は未検証。適用後、混雑帯での停止発生率が 0 になるかを同じ計測系で追跡する予定。

また、以下は未解明のまま残っている:

  • プロセス側で何が蓄積しているかは特定できていない (ミッションスクリプト由来ではないことのみ確定)
  • 10 名以上の高負荷時における停止長の実測データが不足している

[Fix] DCS 2.9.27.24969 dedicated server crashes immediately on Wine (Linux) — msvcp140_atomic_wait.dll stub crash

Wine / Linux DCS 2.9.27.24969 msvcp140_atomic_wait.dll

Environment

  • DCS World Server 2.9.27.24969 (2026-06 時点の最新)
  • Linux 上で Wine + Docker (aterfax/dcs-world-dedicated-server) を使用して起動
  • 影響範囲: Wine ベースの Linux DCS サーバ全般 (DCSSB Option C / Linux-native 含む)
  • 影響 Wine バージョン: wine-8.0 (Debian bookworm デフォルト)、wine-11.0 upstream も初期は未修正

Symptom

DCS 専用サーバ (DCS_server.exe) がログ出力なしに起動直後クラッシュ。Wine が以下を報告:

EXCEPTION_WINE_STUB: 0x80000100

WINEDEBUG=err+module を設定すると:

err:module:import_dll Library MSVCP140_ATOMIC_WAIT.dll (which is needed by DCS_server.exe) not found
err:module:loader_init Importing dlls for DCS_server.exe failed, status c0000135

サーバは dcs.log を書かず、Listening on port XXXXX にも到達しない。

Root Cause

DCS 2.9.27.24969msvcp140.dll の依存関係が更新され、msvcp140_atomic_wait.dll から __std_tzdb_get_sys_info__std_tzdb_delete_sys_info静的インポート するようになった。

これらは C++ STL の chrono タイムスタンプ書式化で使用するタイムゾーン DB クエリ関数。 wine-8.0 および wine-11.0 の msvcp140_atomic_wait.dll はこれらのエクスポートが @ stub のみで、 DCS が DLL ロード時に呼び出そうとすると EXCEPTION_WINE_STUB (0x80000100) が発生してアボートする。

関連する Microsoft STL ABI (_Sys_info 構造体):

struct tzdb_sys_info {
    int32_t error;      // offset 0 (0=success)
    // 4 bytes padding
    double begin;       // offset 8  — range start (epoch ms)
    double end;         // offset 16 — range end   (epoch ms)
    int32_t offset;     // offset 24 — UTC offset in ms
    int32_t save;       // offset 28 — DST offset in ms
    char   *abbrev;     // offset 32 — timezone abbreviation
};                      // total: 40 bytes (_CRT_PACKING=8)

Fix

wine-11.0 ソースから、問題の 2 関数を最小限実装したパッチ済み msvcp140_atomic_wait.dll をビルドして置き換える。

1. spec ファイルの編集 (dlls/msvcp140_atomic_wait/msvcp140_atomic_wait.spec)

2 つの stub エントリを実エクスポートに変更:

@ stdcall __std_tzdb_get_sys_info(ptr long double)
@ stdcall __std_tzdb_delete_sys_info(ptr)

また __std_atomic_*_indirect / __std_atomic_*_cmpxchg16b@ stub エントリがあれば合わせて削除 (別のクラッシュ源)。

2. main.c に実装を追加
struct tzdb_sys_info {
    enum tzdb_error error;
    double begin;
    double end;
    int32_t offset;
    int32_t save;
    char *abbrev;
};

struct tzdb_sys_info * __stdcall __std_tzdb_get_sys_info(const char *name, size_t len, double sys)
{
    struct tzdb_sys_info *info = calloc(1, sizeof(*info));
    if (!info) return NULL;
    info->error  = TZDB_ERROR_SUCCESS;
    info->begin  = -1e15;
    info->end    =  1e15;
    info->offset = 0;
    info->save   = 0;
    info->abbrev = malloc(4);
    if (info->abbrev) {
        info->abbrev[0] = 'U';
        info->abbrev[1] = 'T';
        info->abbrev[2] = 'C';
        info->abbrev[3] = 0;
    }
    return info;
}

void __stdcall __std_tzdb_delete_sys_info(struct tzdb_sys_info *info)
{
    if (!info) return;
    free(info->abbrev);
    free(info);
}
3. ビルドとインストール
# Debian bookworm コンテナ内 (winehq-stable インストール済み):
apt-get source winehq-stable   # または wine-11.0 ソースを tarball で取得
# 上記の編集を適用後:
./configure --without-x --without-freetype --enable-win64
cd dlls/msvcp140_atomic_wait && make

# インストール
cp .libs/msvcp140_atomic_wait.dll     /opt/wine-stable/lib/wine/x86_64-windows/
cp .libs/msvcp140_atomic_wait.dll.so  /opt/wine-stable/lib/wine/x86_64-unix/
4. Wine prefix の修正 (wine-8.0 から移行した場合)

wine-8.0 で初期化した prefix があるときは追加で 2 点の対処が必要:

# a) system32 内の旧 128KB native PE stub を wine-11.0 の 2KB fake PE stub に置換
cp /opt/wine-stable/lib/wine/x86_64-windows/msvcp140_atomic_wait.dll \
   "$WINEPREFIX/drive_c/windows/system32/msvcp140_atomic_wait.dll"

# b) user.reg を修正 — wine-8.0 は DLL を "native" 登録しておりロードが壊れる
sed -i 's/"[*]msvcp140_atomic_wait"="native"/"*msvcp140_atomic_wait"="builtin"/' \
   "$WINEPREFIX/user.reg"

Known Side Effect

DCS ログのタイムスタンプが UTC 固定になる (実装は常に UTC offset=0 を返す)。表示上のみの問題でゲームプレイへの影響なし。

Tested on

  • DCS 2.9.27.24969、wine-stable 11.0.0.0~bookworm-1
  • Linux、Docker、DCSSB Option C (Linux-native bot が Wine DCS を管理)
  • 3 サーバインスタンス全て simulation started, state=ssRunning に到達することを確認