FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()でクラッシュ

FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()から呼ばれる

VectorVM::Exec(ExecArgs, &UESerializeState);

での低頻度、不定期クラッシュを調査しています。

<br/>

症状としては、ByteCode/OptimizedByteCodeがそれぞれ中身が不安定でクラッシュにつながります。

<br/>

そこで質問なのですが、

FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()内で

FScopeLock Lock(&ExecData.OptimizationTask.Lock);

の記述があります。

<br/>

こちらの変数のLockはここでしか見当たらなかったので、ここが並列に実行される可能性があるのかなと見受けられます。

こちらの認識はあっているでしょうか?

<br/>

その場合Lockのスコープが狭く別スレッドで実行中の同じFNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()

でByteCode/OptimizedByteCodeの中身か書き換えられるのでは?と考えています。

<br/>

こちらいかがでしょうか?

再現手順
様々なスペックのハードでエージング

お世話になっております。

詳細な技術調査に基づくご質問をありがとうございます。

本件、開発チーム側と確認を進めておりますので、もう少々お時間を頂戴できますと幸いです。

また、参考までにクラッシュ時のコールスタックをご提供いただけますでしょうか。

(ByteCodeの書き換わりによるクラッシュということで、一様ではないかと存じますが……なにか一例で構いません)

現状、FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()関連のクラッシュの報告が社内外でほとんどなく、

ByteCode/OptimizedByteCodeがいつの間にか書き換わっているという、具体的な症例では、UE5.1ベースで過去に類似報告が一例あったのみ(UE.52以降で再現せず)となっております。

[Content removed]

参考までに、下記の点につきましても可能であれば情報をいただけますと幸いです。

・エージング中の再現とのことでしたが、クラッシュ頻度は体感でどの程度でしょうか?

・CVarでvm.UseOptimizedVMByteCode及びvm.OptimizeVMByteCodeを0に設定して、OptimizedByteCodeを使用しない選択をした場合、クラッシュ頻度に差は出ますでしょうか?

以上、よろしくお願いいたします。

とくにプラットフォーム差のない部分だったのでここで質問しておりますが、特定の2プラットフォームで再現しております。

どちらもNDAプラットフォームのため、ここでコールスタックを記載してもよいものか判断に迷っています。

クラッシュする場所で言うと

void VectorVM::Exec(FVectorVMExecArgs& Args, FVectorVMSerializeState *SerializeState)

のbUseOptimizedByteCodeがfalseの場合にくるVMのオペコードの実行処理で

とあるプラットフォームではswitch文のdefaultに飛んできてfaitalerror(頻度は3~8時間でおちる)

もう一つのプラットフォームではswitch文のオペコードの実行処理でクラッシュ(頻度は1日エージングしていて起きることがある)

その他の情報としては

FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()の

ExecArgs.OptimizedByteCodeがnullになった次の行でログやダンプを吐くとExecData.OptimizedByteCodeの配列に値が入っていたりして、別スレッドから書き換えられているように感じています。

UNiagaraScriptにFCriticalSectionを追加して、FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()全体をLockすると発生しなくなります。

自分でコードを見た限りではスレッドセーブな処理に感じてはいますが、何か見落としがあるのかもしれません。

>>CVarでvm.UseOptimizedVMByteCode及びvm.OptimizeVMByteCodeを0に設定して、OptimizedByteCodeを使用しない選>>択をした場合、クラッシュ頻度に差は出ますでしょうか?

こちら検証してみます。

しばらくお待ちください。

ご回答ありがとうございます。

おっしゃる通り、コールスタックの共有は避けるべき状況であると思います。それでは、今回は頂戴した範囲の情報で十分ですので、これで進めさせていただきます。また、ロック範囲を広げるとクラッシュしなくなったという情報の共有もありがとうございます。大変ありがたい情報です!

これらの情報を開発チームにフィードバックして、さらに議論を進めてまいります。少々お待ちください。

なお、​先に共有したケースと同じくUE5.1の頃のものですが、GCで解放したNiagaraのインスタンスが何らかの原因でまだ走ってしまい、不正なデータをバイトコードを読んでしまっているのではないか、という疑いが議論された投稿が見つかりましたので、参考までに共有いたします。

[Content removed]

開発チームとの確認は、現時点で集まっている情報で進めてまいりますが、​CVarを変更したテストが実施可能で、何か進展がありましたら、ご共有いただけますと助かります。

お手数をおかけしますが、よろしくお願いいたします。

お世話になっております。

開発チームとも協議いたしましたので、ご報告いたします。

まず、元投稿において頂戴したご質問について改めてご回答申し上げます。

> FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()内で

> FScopeLock Lock(&ExecData.OptimizationTask.Lock);

> の記述があります。

>

> こちらの変数のLockはここでしか見当たらなかったので、ここが並列に実行される可能性があるのかなと見受けられます。

> こちらの認識はあっているでしょうか?

はい、その認識で間違いございません。

複数のスレッドがこの関数を同時に実行した場合、ExecData.OptimizationTask.State が IsValid() であれば、各スレッドが if 文に入り、そのうちの1スレッドが FScopeLock を取得してバイトコードの最適化結果を適用(ApplyFinishedOptimization())します。その他のスレッドはロックで待機し、最適化済みコードが適用済みであれば何もせずに進むというのが、想定されている動作です。

> その場合Lockのスコープが狭く、別スレッドで実行中の同じ FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy() で ByteCode / OptimizedByteCode の中身が書き換えられるのでは?と考えています。

こちらは開発チーム側としては想定していない振る舞いで、発生しうるシナリオに心当たりがないとのことです。クラッシュシナリオとしては、

if (ExecData.OptimizationTask.State.IsValid()) { FScopeLock Lock(&ExecData.OptimizationTask.Lock); if (ExecData.OptimizationTask.State.IsValid()) { ExecData.ApplyFinishedOptimization(Script->GetVMExecutableDataCompilationId(), ExecData.OptimizationTask.State); ExecData.OptimizationTask.State.Reset(); } }こちらのコードブロックの最初のifチェックの時点で、あるスレッドのExecData.OptimizationTask.StateオブジェクトがValidではなく、別のスレッドではValidだったということが考えられるのですが、そのような状況自体が想定外(発生しないはず)であり、再現条件の手がかりが欲しいとのことでした。

(ロックの範囲が狭いことが問題なのではなく、このような想定外の状況がなぜ起きているのか、ということのほうが問題という認識です)

また、

> UNiagaraScriptにFCriticalSectionを追加して、FNiagaraScriptExecutionContextBase::ExecuteInternal_Legacy()全体をLockすると発生しなくなります。

ScriptオブジェクトのクリティカルセクションでExecuteInternal_Legacy()全体をLockすると発生しなくなったということは、何らかの不整合の発生がExecuteInternal_Legacy()で実行されるコードの範囲内で起こっていることになりますが、コードをチェックした限りでは、当方でも不審な点を見つけることができておりません。

UE5.4では、社内外で他の報告例を見つけられていないことも気になっております。何か思い当たることがありましたら、情報提供をいただけますと幸いです。

※このスレッドをPrivateに切り替えることも可能ですので、必要に応じてご連絡ください。

なお、UE5.6 ではこの Legacy VM が完全に削除されており、新型VM(UE5.4 時点ではExecuteInternal_Experimental()経由の実行パス)に一本化されました。そのため、今後の流れとしては、ExecuteInternal_Legacy()だけの問題であればローカルパッチで対応、ExecuteInternal_Legacy()の外側に問題がある(新型VMでも問題がある)場合はエンジン側で対応、という形になるかと存じます。

以上、よろしくお願いいたします。

他作業が押していて、以前お約束した検証は少し先になりそうです。

現状、FCriticalSectionで全体をロックするとごく一部パフォーマンスの低下が確認されたので、FRWLockに変更して

FScopeLock Lock(&ExecData.OptimizationTask.Lock);の部分をWriteLock

その直後からReadLockをかけて動作確認をしている最中です。

不具合についてなにか手がかりを見つけたらお知らせいたします。

お世話になっております。

ご確認ありがとうございます。

検証が後回しになる件、承知いたしました。

原因からは少し距離のある検証になる気がいたしますので、本当にお手すきの際で構いません。

また、クリティカルセクションをリードライトロックに変更して排他処理を最小限にとどめる対応を行われている最中だという旨も承知いたしました。

以上、よろしくお願いいたします。