[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
Symptom
いつから起きているか
Method (壊さずに中を測る)
Findings
Root Cause (wine-11.0 heap.c)
確認して外れた仮説
Fix
Status / Verification
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.lua の setUnitsData が 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.lua の setUnitsData の周期 return time + 0.05 を 0.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
Symptom
Method
Finding 1: シムスレッドが E-core に固定されていた
Finding 2: SAM レーダー制御スクリプトの A/B 結果
Not the cause (確認済みの容疑者)
Fix
Result
Status
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.exe を sudo -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/*/status の Cpus_allowed_list) と、実行中コア (ps -o psr) を 1 秒毎 60 回サンプリング
DCSServerBot の core/process/processmanager.py と core/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_setaffinity が EINVAL となり、DCS 起動処理が例外で失敗しうるため。
2. ミッションを原本 (SAM レーダー制御 Lua 有効) に戻す
A/B の B 側は原本より一貫して悪いため、原本に戻した。無人時 (0 人) に serverSettings.lua の current / 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
Symptom
Why it is hard to detect
Method
Findings
Root Cause Isolation
Mitigation
Status
Related (separate issue)
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
Symptom
Root Cause
Fix
Known Side Effect
Tested on
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.24969 で msvcp140.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 に到達することを確認