LocalizationBehaviour.LateUpdate causing big slowdown

Hey,

For some reason my game spikes when performing a specific action, so I decided to deep profile it, and turns out it’s the Localization package using the resources. No real indication as to where or how. Any ideas?

That’s releasing Addressables operations. I dont know why it would cause that though. It looks like it’s coming from the ObjectPools which are not specific to the localization package. Could you please file a bug report so we can investigate? Unity QA: Building quality with passion
Also, try updating to the latest version 1.4.3 if you are not already using it.

I wont be filing a bug report since my Unity crashed whenever I try that, but I can try updating!

If it helps, each time I perform the action, the localization issue increases, for example from 700ms to 1000ms to 1500ms, and so on.

Unfortunately updating didnt solve it. Usually it says what actually calls it but no trace so very hard to debug.

Can you share the project?

Im afraid not due to legal reasons, plus the project is huge 40gb+.

Does the issue happen in a new project? I’m afraid theres not much we can do without a bug report or a project with the issue. The call stack doesn’t tell us why it’s causing the performance issues.

Its becoming a bit unsustainable now, this is in a live game with 100k+ players on Steam, full of quite heavy operations and localization package uses 36% of CPU?

For reference, we have 1000+ unoptimized AI walking around with RVO enabled and they use 16% CPU…

Is there any way we can look at this together? I’m happy to set a short meeting. If this continues I don’t really have any other choice than to use something from the asset store instead of your own solution, which is… disappointing.

9473278--1331587--upload_2023-11-15_17-2-49.png

LocalizationBehaviour is responsible for returning operations back to object pools for reuse. So if you make 1000 calls to it in 1 frame then they will be returned a few frames later. This could be a sign that you need to manage your operations better. Are you using any custom code or just the LocalizationStringEvent component?
One thing we could try and do is to time slice the release operation so it does not run in a single frame but spreads out to avoid spikes.
We can meet to work through this although I don’t think it will be necessary.
Lets first look at how you are calling localization, ill look at making some changes to support time slicing and tell you how to customize the package to incorporate them.

Here are 2 changes you can apply:

First, move the localization package folder out of the PackageCache and into the projects Packages folder. You will now be able to make changes, make sure you commit this to your source control.

  1. Apply throttling to the LocalizationBehaviour: throttling.patch
  2. Improve pool performance, especially in the Editor, this is a bigger change. pools.patch

We will get both of these changes into our next release 1.5, this should be available before the end of the year.

9473602–1331665–throttling.patch.txt (2.53 KB)
9473602–1331671–pools.patch.txt (35.1 KB)

Thank you very much for this, can you explain the process you have in mind when you say to “apply” these .txt files? How do I go about applying them?

As for special operations, I do a lot of localization at runtime, no way around that, so the LocalizationStringEvent is only used for strings that never change.

I use this helper class for translations at runtime:

public static class Utils_Text
{
    public static StringTable GetStringTable()
    {
        return LocalizationSettings.StringDatabase.GetTable("stringTable", LocalizationSettings.SelectedLocale);
    }

    public static string GetLocalizedString(string entryKey)
    {
        string result = "";

        try
        {
            result = GetStringTable().GetEntry(entryKey).GetLocalizedString();
        }
        catch
        {
            Debug.LogError("Could not find localization entry: " + entryKey);
        }

        return result;
    }
}

I realized now I can do some improvements in my end too, I added a cache for the string table:

public static class Utils_Text
{
    public static StringTable stringTable;

    public static StringTable GetStringTable()
    {
        return LocalizationSettings.StringDatabase.GetTable("stringTable", LocalizationSettings.SelectedLocale);
    }

    public static string GetLocalizedString(string entryKey)
    {
        string result = "";

        if(stringTable == null)
        {
            stringTable = GetStringTable();
        }

        try
        {
            result = stringTable.GetEntry(entryKey).GetLocalizedString();
        }
        catch
        {
            Debug.LogError("Could not find localization entry: " + entryKey);
        }

        return result;
    }
}
1 Like

Hi. They are patch files, you can apply them using various software.

Caching the string table will likely solve the issue however if it does not then apply the suggested changes:

I think the simplest thing would be to just apply the throttling, that should do enough.
Replace the contents of LocalizationBehaviour.cs with:

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.Util;

namespace UnityEngine.Localization
{
   class LocalizationBehaviour : ComponentSingleton<LocalizationBehaviour>
   {
       Queue<(int frame, AsyncOperationHandle handle)> m_ReleaseQueue = new Queue<(int, AsyncOperationHandle)>();

       const long k_MaxMsPerUpdate = 10;
       const bool k_DisableThrottling = false;

       protected override string GetGameObjectName() => "Localization Resource Manager";

       /// <summary>
       /// To prevent you having to explicitly release operations, Unity does this automatically a frame after the operation is completed.
       /// If you plan to keep hold of a reference, call <see cref="AsyncOperationHandle.Acquire"/>, and <see cref="AsyncOperationHandle.Release"/> when it's finished.
       /// </summary>
       /// <param name="handle"></param>
       public static void ReleaseNextFrame(AsyncOperationHandle handle) => Instance.DoReleaseNextFrame(handle);

       [MethodImpl(MethodImplOptions.AggressiveInlining)]
       static long TimeSinceStartupMs() => (long)(Time.realtimeSinceStartup * 1000.0f);

       void DoReleaseNextFrame(AsyncOperationHandle handle)
       {
           enabled = true;
           m_ReleaseQueue.Enqueue((Time.frameCount, handle));
       }

       void LateUpdate()
       {
           var currentFrame = Time.frameCount;
           long currentTime = TimeSinceStartupMs();
           long maxTime = currentTime + k_MaxMsPerUpdate;
           while (m_ReleaseQueue.Count > 0 && m_ReleaseQueue.Peek().frame < currentFrame)
           {
               currentTime = TimeSinceStartupMs();
               if (!k_DisableThrottling && currentTime >= maxTime)
               {
                   // We spent too much time on this frame, we break for now, we'll resume next frame
                   break;
               }

               var item = m_ReleaseQueue.Dequeue();
               AddressablesInterface.SafeRelease(item.handle);
           }

           if (m_ReleaseQueue.Count == 0)
               enabled = false;
       }

       public static void ForceRelease()
       {
           foreach(var r in Instance.m_ReleaseQueue)
           {
               AddressablesInterface.SafeRelease(r.handle);
           }
           Instance.m_ReleaseQueue.Clear();
       }
   }
}
1 Like

Thanks a lot for the help :slight_smile:

1 Like