Performance issues with frequently updated text block

Hi there! I have a chat-like window in my game that frequently gets updated with text and am having some performance related issues when the update frequency is relatively high.

Any time text is added it gets appended to the end of a string builder. The text then gets set on the TMP object via SetCharArray to avoid GC alloc as much as possible. I should also mention that my text object is the content for a UGUI scroll view. I had originally had a content size fitter attached so that the scroll view would dynamically resize, but that caused a lot of performance problems by calling GetPreferredSize too frequently. Instead I do a very rough estimation of the desired height (assuming no line wrapping) via the font size, line spacing, etc and resize the rect accordingly so that the scroll bar is sized appropriately. However, it appears that every time I change the scroll rect size it triggers TextMeshProUGUI.OnRectTransformDimensionsChange() which in turn seems to trigger this set of events:


(this was profiling a built client, not the editor. probably about 200+ lines of text on the TMP object)

Does anyone have any thoughts on how I might get around these nasty performance hitches when resizing the scroll rect? More generally what is the most efficient way of handling a TMP object with frequent updates while maximizing performance?

Whenever the RectTransform size changes, we need to do a whole new layout pass on the text. Since the performance overhead of the layout scales with the amount of text, as you add more text, the slower it will get as we have to parse and re-layout all of the text.

My suggestion to handling this is to track lines of text individually in some list and to break up the text into multiple text objects that are part of some object pool so you only end up with roughly the same amount of text objects as you have lines of text visible in this scroll area. By line of text, I don’t mean a physical line but a line in the sense of first character to linefeed.

As you scroll up and down, you will need to make sure each text object is positioned correctly relative to the previous one. For most text objects moving up or down will simply be a positional adjustment on their transform and not require a new layout pass. For the new text objects becoming visible, you will need to set their text which will require a new layout pass.

When the RectTransform width changes, all visible text objects will require a new layout pass but assuming only 10 of them are visible, that is re-layout on essentially 10 lines vs. 200 lines so much more efficient.

Although this requires a more complex setup, it should provide much better overall performance.

1 Like

Would nesting the TMP object with a max size rect under the scroll view’s content rect still trigger the TMP resize when it’s parent is resized?

This is something I had done previously but I ran into issues with some lines bleeding into other lines when they wrapped. I assumed GetPreferredHeight wasn’t returning the correct value when I asked for it but I was unable to reliably reproduce the problem. Things just feel cleaner as a single block of text and avoided the overlap possibility entirely. But as you said, I may have to revisit this to avoid this nasty performance bottleneck.

edit: I should also say thanks for the quick response!

Resizing of the height of the RectTransform of the scroll view should not affect text layout.

Resizing the width would depend on the RectTransform settings where for instance if the RectTransform was using Stretch Anchor mode, it could affect the RectTransform of the child text objects. In that case, it would trigger OnRectTransformDimensionsChange() and a re-layout. However, if the text object’s RectTransforms are unaffected that it should not trigger this.

P.S. Be mindful that Stretch Anchor mode can result in floating point errors where although the RectTransform hasn’t really changed size, it still triggers OnRectTransformDimensionsChange().

In terms of lines over lapping, the GetPreferredValues() function should return the correct width and height but not sure if I would use it or instead process each text object one at a time and using ForceMeshUpdate() to make sure the first text object returns the correct values in the textInfo and then proceed to dealing with the 2nd text object.

The order in which the text objects are processed would vary depending on whether you are handling someone adding a new line of text at the bottom where in that case I would process that object first and then move to the next above. If someone was inserting text in the middle, then I would handle the active one first and then go up until top of scroll view visible area and then down until bottom of scroll view visible area. And similar for other scenarios but in all cases, set text on the active object. ForceMeshUpdate() where you now have correct textInfo data to know everything so you can place the other objects relative to this first one.

1 Like

When dealing with individual lines of text my methodology was the following. I have a disabled prefab (the one that is used to display each line) within the scroll rect anchored accordingly to get the proper RectTransform values. I then feed the disabled prefab’s TMP component my text content and ask for the preferred width/height which I save off for later to do the final adjustments.

var contents = <string value is here>;
var preferred = m_samplerText.GetPreferredValues(contents, m_samplerText.rectTransform.rect.width, m_samplerText.rectTransform.rect.height);
heights[i] = preferred.y;

Should I change this to something like this?

var contents = <string value is here>;
m_samplerText.text = contents;
m_samplerText.ForceMeshUpdate(true);
heights[i] = m_samplerText.preferredHeight;

GetPreferredValues does require a layout pass (ish) but it should return the correct values.

Provided you don’t change any other properties of the text object after calling ForceMeshUpdate(), it should be more efficient as you are skipping the layout (ish) pass from the GetPreferredValues().

1 Like

I just did some testing of this. If I call .preferredHeight, then resize the window (which changes the sample prefab’s width as it is set to stretch with the parent window), call ForceMeshUpdate(true) again, and finally ask for .preferredHeight again it returns the same value as before the resize. However, if I use the .GetPreferredValues() method after the resize that returns the correct value. I might have to just eat that performance cost.

What version of the TMP package are you using?

2.1.6. Working in Unity 2019.4.x HDRP

Just for testing and on a backup of your project, try version 2.2.0-preview.1 as I did make a change to the GetPreferredValues(string, float, float).

From the change log

  • Revised GetPreferredValues(string, float, float) where it will now return the width of the longest line of text given the width restrictions.

For new visitors, want to point out my specific situation

Configuration

  • Canvas.RenderMode == Camera
  • CanvasScaler.UIScaleMode == ScaleWithScreenSize
  • CanvasScaler.Match == 0.5f
  • frequently changing Camera.fieldOfView

make TMPro under this Canvas rebuild, when Camera.fieldOfView changed

Only change

  • CanvasScaler.Match == 1.0f

fixed this