Search Unity

TextMesh Pro SetText(StringBuilder) without allocation - request & workaround

Discussion in 'UGUI & TextMesh Pro' started by OndrejP, Feb 9, 2020.

  1. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    Could you please modify SetText(StringBuilder) to set text without allocations?
    Currently it calls StringBuilder.ToString() which defeats the whole purpose of using StringBuilder.

    We're using custom StringBuilder extensions to do most text concat. without any allocations and this is the last part of the chain which still allocates memory every time text is changed.

    What about storing StringBuilder internally and only update m_text when it's required (e.g. someone calls 'text' getter). I've checked the source and 'm_text' is barely used. This would've solve it.

    In the meantime we use this unsafe method to directly modify contents of the string.
    It's not pretty, but it works well (until someone calls 'text' getter and stores the string for some other purpose).

    Code (CSharp):
    1. private static char[] m_tmpArray = new char[2048];
    2.  
    3. /// <summary>
    4. /// Sets text without allocations by directly modifying internal string.
    5. /// Uses intermediate char array to get characters from StringBuilder faster.
    6. /// </summary>
    7. public static unsafe void SetTextNoAlloc(this Text text, StringBuilder stringBuilder)
    8. {
    9.     // Get internal string
    10.     var str = text.text ?? string.Empty;
    11.     int builderLength = stringBuilder.Length;
    12.  
    13.     // Make sure array is large enough (extra char for zero-terminator)
    14.     if (m_tmpArray.Length < builderLength + 1)
    15.     {
    16.         m_tmpArray = new char[MathUtil.UpperPowerOfTwo(builderLength + 1)];
    17.     }
    18.  
    19.     // Copy stringBuilder to array
    20.     stringBuilder.CopyTo(0, m_tmpArray, 0, builderLength);
    21.  
    22.     // Zero-terminate
    23.     m_tmpArray[builderLength] = default;
    24.  
    25.     if (str.Length < builderLength)
    26.     {
    27.         // String is not large enough, create new from array
    28.         int capacity = Math.Min(m_tmpArray.Length, MathUtil.UpperPowerOfTwo(builderLength));
    29.         str = new string(m_tmpArray, 0, capacity);
    30.     }
    31.     else
    32.     {
    33.         // Copy from array to string, when string is longer than builder, include zero-terminator
    34.         int copyCount = str.Length > builderLength ? builderLength + 1 : builderLength;
    35.         fixed (char* pStr = str)
    36.         fixed (char* pTmpArray = m_tmpArray)
    37.         {
    38.             UnsafeUtility.MemCpy(pStr, pTmpArray, sizeof(char) * copyCount);
    39.         }
    40.     }
    41.  
    42.     // Assign string
    43.     text.SetText(str, true);
    44. }
     
  2. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    These allocations only occur in the Editor as seen below to keep the Inspector in sync.

    Code (csharp):
    1.  
    2. #if UNITY_EDITOR
    3.     // Set the text in the Text Input Box in the Unity Editor only.
    4.     m_text = text.ToString();
    5. #endif
    6.  
     
  3. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    Okay, thanks. I'm stupid, checked the decompiled code, not the source :)
     
  4. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    Would you consider adding this SetText method?

    Code (CSharp):
    1. public void SetText(StringBuilder text, bool syncTextInputBox)
    This would allow me to profile GC allocations in editor without too much clutter and it would work in same manner as the method taking string and bool.
     
  5. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,091
    +1

    Same thing for SetCharArray
     
    OndrejP likes this.
  6. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    I'll take a look at adding these in the next minor release which will be version 1.6.x for Unity 2018.4, version 2.2.x for Unity 2019.4 and version 3.1.x for Unity 2020.x.
     
    OndrejP likes this.
  7. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,091
    Looking at source code is seems that this makes a bug outside of editor as text property is defined as:
    Code (CSharp):
    1. public virtual string text
    2.         {
    3.             get { return m_text; }
    So this won't work correctly:
    Code (CSharp):
    1. tmpText.SetText( new char[] { 'a' });
    2. Debug.Log( $"tmpText.text: {tmpText.text}" );
    I guess cleaner implementation could be:
    Code (CSharp):
    1. public virtual string text
    2. {
    3.     get
    4.     {
    5.         if ( isTextStringDirty )
    6.             m_text = BuildTextString();
    7.         return m_text;
    8.     }
    Then TMP inspector editor would have to check isTextStringDirty and rebuild if needed. Old logic still would have to be there when using SetText in case of !EditorApplication.isPlaying.

    Another bug, seems like parseCtrlCharacters isn't implemented for SetText(char[]) (haven't checked but that's what I understood from reading code)? Both of char[] and StringBuilder have
    public char this[int index]
    so I guess that code could be unified.
     
    Last edited: Nov 3, 2020
  8. realcosmik

    realcosmik

    Joined:
    Nov 27, 2018
    Posts:
    20
    +1 for this thread just profiled and seen setText with a string builder allocs, I was like huhhhhhhh