【问题标题】:Framerate lag spikes from network requests on main thread主线程上网络请求的帧率延迟峰值
【发布时间】:2021-10-18 02:57:29
【问题描述】:

我目前正在开发一个使用 OpenAI 的 GPT3 为 NPC 对话提供支持的项目。每个 NPC 在对话时会向我的服务器发送一个 POST 请求,该请求会返回 NPC 通过其本地 AudioSource 播放的音频文件。

视频在这里:https://www.youtube.com/watch?v=pygM6yDE9hI

我遇到的一个问题是抖动和短暂的延迟。

我是 Unity 的新手,但根据分析器,我认为罪魁祸首是处理网络请求的协程。

下面是我的这个功能的代码:

     // Update is called once per frame
 void Update()
 {
     int index = -1;
     if (conversation != null)
     {
         if (!conversation.processing)
         {
             if (conversation.currentSpeaker.Equals(this.id))
             {
                 Debug.Log(this.id + " getting response");
                 conversation.processing = true;
                 StartCoroutine(getResponse(conversation));
             }
         }
     }
     else if (Datastore.Instance.id2conversation.TryGetValue(id, out index))
     {
         conversation = Datastore.Instance.conversations[index];
     }
 }

 IEnumerator getResponse(Conversation conversation)
 {
     WWWForm form = new WWWForm();
     form.AddField("id", this.id);
     var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

     yield return www.SendWebRequest();

     if (interrupted) yield break;

     if (www.isNetworkError)
     {
         Debug.Log(www.error);
     }
     else
     {
         if (www.GetResponseHeaders().Count > 0)
         {
             var jsonData = JSON.Parse(www.downloadHandler.text);

             string stringData = jsonData["audioContent"]["data"].ToString();
             byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);

             AudioClip clip = AudioHelpers.ConvertToAudioClip(rawdata);

             this.audioSource.clip = clip;
             this.audioSource.Play();

             this.animator.SetBool(this.talkingBoolHash, true);

             Debug.Log("Response Recieved");

             yield return new WaitForSeconds(clip.length);

             if (interrupted) yield break;

             this.conversation.currentSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
             this.conversation.processing = false;
             this.animator.SetBool(this.talkingBoolHash, false);
         }
     }
 }

如何提高此代码的性能以消除帧速率下降的时期?

是否可以通过 Unity Jobs 将此代码移动到另一个线程?

任何帮助将不胜感激。

编辑: 深度剖析器:

为 JSON 解析实现了线程。新代码:

// Update is called once per frame
    void Update()
    {
        int index = -1;
        if (conversation != null)
        {
            if (!conversation.processing)
            {
                if (conversation.currentSpeaker.Equals(this.id))
                {
                    Debug.Log(this.id + " getting response");
                    conversation.processing = true;
                    StartCoroutine(getResponse(conversation));
                }
            }
        }
        else if (Datastore.Instance.id2conversation.TryGetValue(id, out index))
        {
            conversation = Datastore.Instance.conversations[index];
        }

    }

    Task<(byte[], string)> ParseAudioData(string rawJson)
    {
        try
        {
            return Task.Run(() => {
                var jsonData = JSON.Parse(rawJson);
                string stringData = jsonData["audioContent"]["data"].ToString();
                byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);
                string nextSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
                return Task.FromResult((rawdata, nextSpeaker));
        });
    }
    catch(Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}

    IEnumerator getResponse(Conversation conversation)
    {
        WWWForm form = new WWWForm();
        form.AddField("id", this.id);

        var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

        yield return www.SendWebRequest();

        if (interrupted) yield break;

        if (www.isNetworkError)
        {
            Debug.Log(www.error);
        }
        else
        {
            if (www.GetResponseHeaders().Count > 0)
            {
                Task<(byte[], string)> t = ParseAudioData(www.downloadHandler.text);

                yield return t;

                AudioClip clip = AudioHelpers.ConvertToAudioClip(t.Result.Item1);

                this.audioSource.clip = clip;
                this.audioSource.Play();

                this.animator.SetBool(this.talkingBoolHash, true);

                Debug.Log("Response Recieved");

                yield return new WaitForSeconds(clip.length);

                if (interrupted) yield break;

                this.conversation.currentSpeaker = t.Result.Item2;
                this.conversation.processing = false;
                this.animator.SetBool(this.talkingBoolHash, false);
            }
        }
    }

最终编辑:在下面的答案的帮助下得到了它!

我的最终代码:

Task<JSONNode> ParseJsonData(string rawJson)
{
    try
    {
        return Task.Run(() =>
        {
            JSONNode jsonData = JSON.Parse(rawJson);
            return jsonData;
        });
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}

Task<(byte[], string)> ParseAudioData(JSONNode jsonData)
{
    try
    {
        return Task.Run(() => {
            string stringData = jsonData["audioContent"]["data"].ToString();
            byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);
            string nextSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
            return (rawdata, nextSpeaker);
        });
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}
IEnumerator PlayDialog(AudioClip clip, string nextSpeaker)
{
    this.audioSource.clip = clip;
    this.audioSource.Play();
    this.animator.SetBool(this.talkingBoolHash, true);

    yield return new WaitForSeconds(clip.length);

    if (interrupted) yield break;

    this.conversation.currentSpeaker = nextSpeaker;
    this.conversation.processing = false;
    this.animator.SetBool(this.talkingBoolHash, false);
}

IEnumerator getResponse(Conversation conversation)
{
    WWWForm form = new WWWForm();
    form.AddField("id", this.id);
    var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

    yield return www.SendWebRequest();

    if (interrupted) yield break;

    if (www.isNetworkError)
    {
        Debug.Log(www.error);
    }
    else
    {
        if (www.GetResponseHeaders().Count > 0)
        {
            ParseJsonData(www.downloadHandler.text).ContinueWith((jsonData) => {
                ParseAudioData(jsonData.Result).ContinueWith((t) =>
                {
                    // https://github.com/PimDeWitte/UnityMainThreadDispatcher
                    UnityMainThreadDispatcher.Instance().Enqueue(() =>
                    {
                        AudioClip clip = AudioHelpers.ConvertToAudioClip(t.Result.Item1);
                        StartCoroutine(PlayDialog(clip, t.Result.Item2));
                        Debug.Log("Response Recieved");
                    });
                });
            });
           
        }
    }
}

【问题讨论】:

  • 不理解return Task.FromResult。由于我不会进入的原因,通常应该避免这种方法。您是否尝试使用以下答案?如果是,有什么问题?这在我的脑海中有点难以理解,但你不想阻止在后台运行的任务。
  • @Zer0 我无法直接复制并粘贴下面的答案,因为 ContinueWith 中的代码在范围内没有 rawdata 变量。此外,当我通过一些小的调整让它工作时,什么都没有发生,我天真地假设这是因为你不允许在其他线程中调用 Unity API。对 C# 语法仍然非常缺乏经验。应该使用什么来代替 Task.FromResult?
  • 尝试将TaskScheduler.FromCurrentSynchronizationContext() 作为ContinueWith 的参数,让它在主线程上运行。我不熟悉 Unity,所以如果这不起作用,有人可以纠正我。如果没有,可以试试async await 吗?
  • @Zer0 你对 TaskScheduler.FromCurrentSynchronizationContext() 有什么建议吗?之前没见过,不太明白怎么实现。

标签: c# unity3d optimization task


【解决方案1】:

我会说延迟峰值可能来自 JSON 解析并将其转换为 AudioClip。您可能可以在不同的线程上执行此操作:

Task<byte[]> ParseJsonData (string rawJson)
{
    try
    {
        return Task.Run(() =>
        {
            jsonData = JSON.Parse(rawJson);
            string stringData = jsonData["audioContent"]["data"].ToString();
            return AudioHelpers.ConvertToByteStream(stringData);
        });
    }
    catch (Exception e)
    {
        UnityEngine.Debug.LogException(e);
        throw;
    }
}

然后这样称呼它:

IEnumerator getResponse(Conversation conversation)
{
    WWWForm form = new WWWForm();
    form.AddField("id", this.id);
    var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

    yield return www.SendWebRequest();

    if (interrupted) yield break;

    if (www.isNetworkError)
    {
        Debug.Log(www.error);
    }
    else
    {
        if (www.GetResponseHeaders().Count > 0)
        {
            ParseJsonData(rawJson)
                .ContinueWith(ParseAudioData);
            ParseAudioData(www.downloadHandler.text).ContinueWith((rawData) =>
            {
                // https://github.com/PimDeWitte/UnityMainThreadDispatcher
                UnityMainThreadDispatcher.Instance.Enqueue(() =>
                {
                    AudioClip clip = AudioHelpers.ConvertToAudioClip(rawData);
                    StartCoroutine(PlayDialog(clip));
                    Debug.Log("Response Recieved");
                });
        }
    }
}

IEnumerator PlayDialog (AudioClip clip)
{
    this.audioSource.clip = clip;
    this.audioSource.Play();
    this.animator.SetBool(this.talkingBoolHash, true);
    yield return new WaitForSeconds(clip.length);
    if (interrupted) yield break;
    this.conversation.currentSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
    this.conversation.processing = false;
    this.animator.SetBool(this.talkingBoolHash, false);
}

这会将操作发送到新线程并在操作结束时继续执行您的代码。如果延迟峰值来自解析 json,这应该会有所帮助。从其他线程运行代码时要小心,因为您无法访问 Unity 的大部分功能并且它们都是异步的。

编辑:包括一些用于在 MainThread 上运行代码的实用程序,因为正如其他人指出的那样,如果不摆弄同步上下文,这将无法工作。

另外,我建议您尝试将您的方法与单一职责分开。到目前为止,您的协程正在下载内容、解析它、播放音频和更新对话状态。这对于单个函数来说是相当多的。

你的 JSON 也应该是一个结构化的类,解析它并仍然通过它的字符串哈希访问东西是没有意义的。

如果这仍然不能解决您的问题,您可能需要更深入地了解分析器,并准确检查导致它在主线程中花费如此多时间的原因。

【讨论】:

  • 我建议使用throw; 而不是throw e;,这样您就不会弄乱堆栈跟踪。
  • 谢谢!目前正在努力实现这一点。 ContinueWith 是如何工作的?它下面的代码在哪里可以访问 rawdata 变量?
  • ContinueWith 是在Task 以一种或另一种方式完成之后运行的东西。您可以指定它应该何时运行(成功、错误等)。这基本上就是 async await 在幕后工作的方式。
  • 能否在 ContinueWith() 中调用 Unity API 方法?
  • @Zer0 进行了编辑以包含我的新代码。我认为它运行得更快,但仍然有明显的尖峰,我想知道我是否错误地实现了某些东西。据我了解,Unity API 无法从其他线程访问,这就是我按照我的方式编写更新代码的原因。我该如何改进它?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-02
  • 2011-01-13
  • 1970-01-01
  • 2012-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多