Large GC calling 'TMP_Text.GetPreferredHeight()'

I have found out that calling ‘TMP_Text.GetPreferredHeight()’ produce a lot of garbage:

I find it a bit excessive, even if one of the 2 texts where it is called is quite large.
I know that some garbage generated is coming from the editor, but I’m not sure for this one.
I also tried it in a standalone debug build and the garbage indicated was the same in the profiler.

Is there a way to reduce it?

To help me reproduce this, can you provide a test scene / repro project or code used to test this?

If you can’t provide some repro project / scene then please provide the setting used on the text object and the raw text.

It’s quite easy, each time a text with several lines is changed and you get its height it does that.
You will find a tiny repro project attached to this post.
Steps to reproduce:

  • unizip the ‘TMP_GC.zip’ file
  • open the project with Unity 2018.3.0f2 (I’m pretty sure any Unity version will do)
  • open the ‘SampleScene’ scene
  • open the profiler and configure it to deep profile
  • start the project
  • press the ‘Generate GC’ button a few times
  • observe in the profiler that there are peaks of GC when the button is pressed

Note that this example also generate GC because of the canvas, the events, and the string concatenation.
But you will see the same GC generated by ‘preferredHeight’ as in my initial post (here they are coming from the ‘OnClickTest()’ method.
In this example it’s more around 40Ko instead of 1Mo because the text has a lot less lines, but it should be enough to find out where the garbage come from.

I’m not sure if it’s possible to remove it, but if it is it would be a good news.

Also, note that I haven’t checked ‘preferredWidth’.

4148245–365452–TMP_GC.zip (1.07 MB)

In the example you provided, the string concatenation are the cause of the GC.

Try the following script and as you will see, there is no GC resulting from the use of preferredWidth or preferredHeight.

using UnityEngine;
using TMPro;

public class Test : MonoBehaviour
{
    public TextMeshProUGUI TextComponent;
    private string m_TextContent = "A simple line of text.";

    private void Update()
    {
        TextComponent.text = m_TextContent;
        float width = TextComponent.preferredWidth;
        float height = TextComponent.preferredHeight;
    }
}

Accessing these two properties should not be generating any GG so something else is causing the behavior you are reporting.

See if you are doing any string concatenation in your implementation as that would result in GC.

Are you changing the text from frame to frame? If so how and what is it changing to and from?

For instance, as the text grows in size, TMP has to increase its buffer allocations to accommodate for the larger amount of text. These allocations are done in blocks to avoid the frequency of this. When the text shrink back in size (in terms of number of characters), TMP will degenerate the unused vertices to again avoid any resizing / GC resulting from this. However, should the text change enough where you have a variance of more than 256 characters, then TMP will resize those buffers as the allocated geometry is now too excessive.

Normally the handling of buffers described above is not an issue unless, for instance your text object goes from a few characters on one frame and then lots of characters the next frame and then back down to few characters which in turn would results in a constant resizing of these buffers.

I have no idea if this is actually what is happening here but just offering this information to help provide a better understanding of what is going on under the hood.

1 Like

Thanks for looking into it.

So it is the internal arrays containing the vertices which take 23Ko for a text of a dozen characters.
I found it a bit excessive but if the minimum array size is 256, it means about 23 32 bits values, which is probably just that.
And it’s comforting to know that if the size of the text doesn’t change by much there won’t be any additional allocations.

My real-life case is that I add new lines to an output quite often, meaning that I create new Text objects and then fill them with some text. I do not modify the content after that, only the ‘bold’ state when the line is selected/unselected.
Is there a way to force a pre-allocation of a given size in order to avoid garbage when we know in advance how many characters we’ll add?

Most pools and collections do have such an option and it could be useful in this specific case.

I suppose I could also change my code in order to avoid adding a new ‘TextMeshProUGUI’ object each time I want to add a new item in the output, but it’s so much easier as I want to be able to bold/unbold an item when they are browsed, doing that with only one large text and one ‘TextMeshProUGUI’ is possible with RichText, but it’s going to be quite messy on the string modifications…

The initial allocation of buffers is for 8 characters. From there the buffers grow by power of two until 1024 characters at which point they grow by 256 character increment.

The geometry includes vertices, uv0, uv2, color, normal, tangent and triangle index. There are other allocation required beside those including the TextInfo which contains information about the text including info about the characters, words, lines, links, and geometry.

Again these buffers will keep growing with the text but at some point if the size of the text is reduced by more than 256 characters, these buffers will be resized down to whatever is required / next power of two above that.

If the text size stays within the allocations, zero GC occurs even if the text changes every single frame.

My suggestion is to use a pool of text objects to handle all these lines of text. The number of text objects required will be whatever is needed to fill the view area vertically.

As text objects move / scroll out of view, they get recycled. Since each of these text objects will have their own allocations, they will all be susceptible to resizing but if they each contain perhaps a single line of text or a few, these buffers should settle / no longer need changes unless the number of characters will vary greatly between each line.

Let’s say you were creating some text / code editor. You would have some text file / data structure that contains all the text. You would parse the entire text to get a list of lines separated by linefeed…

Then when it is time to display the text, you would feed the first line of text to the first text object. You would query the textInfo and whichever appropriate sub structure like lineInfo to determine the height of this first line. This would allow you to determine where the subsequent line would be positioned. You would then get the 2nd line from the list and feed its content to the 2nd text object and then repeat the process until the view area if filled vertically.

Since each line of text is independent from the others, even if the text was comprised of 1000 lines of text, you would only need a number of text objects equal to whatever is needed to fill the view vertically.

As you scroll up / down, only the newly visible line will need re-parsing / layout to display its content. All other lines are simply moved but since their content hasn’t changed, this will be very efficient in terms of performance. Most likely, only one line of text will change at any given point.

Your view manager will keep track of what lines are visible from the list of lines. It will manage the text objects, their positions to construct the chain of text objects to fill the view. You might need to manage deleting or inserting new lines in the list but something like that should do the trick and be very efficient.

This is just sort of a rough overview. Hopefully it proves useful. Now time for me to go sleep since it is 4:32 am.

P.S. If I had the time, I would create one of those as an example as this is frequently requested for chat systems / code editors / etc.

Thanks for the explanations about how it works internally, it’s always interesting to know how things are working under the hood in order to use them correctly.

For example, I already pool the text objects, but it’s not as powerful as what you describe.
I just have them all in memory, and re-use them when I clear the content.
It is for a debug console, so I have 3 such lists, the history and output, which grows until the user clear them, and the suggestion list which never stops being filled/cleared.

You said that if the number of characters decrease then the array will be downsized.
If I understand that well, it means that if I clear the text when I put back a ‘TextMeshProUGUI’ to its pool, then the array is completely downsized to 8 characters, meaning that I will get garbage when I store it in the pool and when I get it from the pool and put some text in it again, is that it?
If my analysis is correct, then the way I’m doing it is 2 times worse as not pooling the ‘TextMeshProUGUI’ objects (except that there are other things like object creations where it’s a good thing to pool).

If it is the case, I really think that it would a great idea to make that behaviour toggleable.
There are cases where we absolutely do not want the arrays to be downsized as we have to alternate between texts of really different sizes.
I understand the downsizing logic for mobile, but on Windows we have enough memory to survive without downsizing until the ‘TextMeshProUGUI’ objects are garbage collected.

If I understand well the description you made, I should not have several lines in a ‘TextMeshProUGUI’ object, but instead I should have 1 ‘TextMeshProUGUI’ per line of text in order to avoid downsizing problems (all the lines should have quite similar length of course).
Also, I should have only enough ‘TextMeshProUGUI’ objects to be displayed in the panel, and change their content in order to simulate a scrolling.

I guess that there’s more work than meet the eye to create a ListView behaving as you’re describing, as there must be some weird cases to manage, especially to display the scrollbar with the correct size and the correct position.
I am pretty sure there are several assets already doing that, I’ll check them to see if I can find one which is optimized enough.

Thanks again for your help.
Providing an example for a chat system or code editor would certainly help a lot of people, but I don’t think it’s your job.
As I said, there are probably assets already doing that.

But, as I said previously, having a way to prevent the downsizing of the internal arrays would help greatly as it would make pooling ‘TextMeshProUGUI’ objects really efficient.

This reduction in geometry buffer allocation will occur if the difference in characters between the previous and new text is greater than 256. So if several of text objects end up over the 256 threshold and that you set the text to null or string.empty before putting them back into the pool then this is inadvertently resulting in unnecessary GC.

You are not the first user to set the text to null or string.empty in this type of use case and reflecting on this, I made a minor change where the reduction in buffer allocations will not occur if the next text requires zero characters. This way setting the text to null or string.empty will not trigger this and if the new text is within this 256 margin, it will not trigger it either.

Correct. One important part to clarify here is that one line means the characters separated by a linefeed. As such your internal manager tracks lines based on this definition but depending on the view area width, individual text objects may render the text over multiple lines due to word wrapping. So even thought the text renders visually over multiple lines, it is still one single line of text.

Correct as well except that once the text is set in a text object, it never changes until that text object goes outside the view area (or if it is possible to edit text objects). The text containers / RectTransforms are what is moved up and down to simulate scrolling. So in theory, as a new line of text becomes visible, the text is set on that text object while the position of all others is adjusted.

P.S. In addition to the change where reducing buffer allocations will not occur when the text is set to null or string.empty, I’ll consider adding a new property to provide control over this.

Thanks, I understand that few users will probably every use it, so the default will be to downsize the array automatically, but it would be of great help for the few of us who have a need to be able to turn that off.
Note that this property may even not be displayed in the inspector, as it is closely linked to some advanced coding, there’s no need to clutter the UI with things only useful in the code.

New property will be :

public bool vertexBufferAutoSizeReduction

This property will be set to true by default and not exposed in the text object inspector.

Thanks!

Sorry for tagging along here, but this thread seems to be good for asking question about the gc in textmeshpro.

I am looking at the profiler and it seems that using richtext for processing tags such as color and font could possibly be a cause for a large amount of gc generation?

I just want to understand that TMP Parse Text bit is what it is doing (rich texting) and if so that is the actual cause?
If so is there any way to reduce gc but still using rich texting?