libgdx / libgdx

Desktop/Android/HTML5/iOS Java game development framework
http://www.libgdx.com/
Apache License 2.0
23.35k stars 6.44k forks source link

High RAM usage when displaying long texts #7508

Open Sesu8642 opened 2 days ago

Sesu8642 commented 2 days ago

Issue details

In my game, I display the license texts of the free / open source dependencies. To do so, I currently use a label inside a scroll pane. The size of the license texts is quite large, around 3.7 MB. As soon as i show the screen, the RAM usage increases by several 100 MB and causes an OutOfMemoryError on Android devices. I also tried using a TextArea which has the same issue but to a lesser extend.

Reproduction steps/code

In this minimal example, the ram usage increases from around 100 MB to 1.8GB despite the text content itself only being 10 MB.

public class Main extends ApplicationAdapter {
    private Stage stage;

    @Override
    public void create() {

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            stringBuilder.append("aaaaaaaaa\n");
    }

        String text = stringBuilder.toString();
        System.out.println("string size: " + text.getBytes().length + " bytes"); // size is 10 MB

        stage = new Stage(new ScreenViewport());

        FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("FreeSans.ttf"));
        FreeTypeFontParameter parameter = new FreeTypeFontParameter();
        parameter.size = 16;
        BitmapFont font16 = generator.generateFont(parameter);
        generator.dispose();
        LabelStyle labelStyle = new LabelStyle(font16, Color.WHITE);
        Label label = new Label(text, labelStyle);

        stage.addActor(label);
    }

    @Override
    public void render() {
        ScreenUtils.clear(0.15f, 0.15f, 0.2f, 1f);
        float delta = Gdx.graphics.getDeltaTime();
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        stage.act(delta);
        stage.draw();
    }

    @Override
    public void dispose() {
        stage.dispose();
    }
}

demo.zip

Version of libGDX and/or relevant dependencies

tested with libGDX 1.13.0 and 1.12.1

Stacktrace

11-10 20:59:03.691 32570 32607 E AndroidRuntime: java.lang.OutOfMemoryError: Failed to allocate a 290269856 byte allocation with 100663296 free bytes and 118MB until OOM, target footprint 244968048, growth limit 268435456
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.graphics.g2d.BitmapFontCache.requirePageGlyphs(BitmapFontCache.java:344)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.graphics.g2d.BitmapFontCache.requireGlyphs(BitmapFontCache.java:318)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.graphics.g2d.BitmapFontCache.addToCache(BitmapFontCache.java:382)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.graphics.g2d.BitmapFontCache.addText(BitmapFontCache.java:531)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.graphics.g2d.BitmapFontCache.setText(BitmapFontCache.java:486)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Label.layout(Label.java:223)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Widget.validate(Widget.java:88)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Table.layout(Table.java:1165)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup.validate(WidgetGroup.java:104)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Container.layout(Container.java:153)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup.validate(WidgetGroup.java:104)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Stack.layout(Stack.java:107)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup.validate(WidgetGroup.java:104)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.ScrollPane.layout(ScrollPane.java:498)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup.validate(WidgetGroup.java:104)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Table.layout(Table.java:1165)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup.validate(WidgetGroup.java:104)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.ui.Table.draw(Table.java:108)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.Group.drawChildren(Group.java:111)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.Group.draw(Group.java:58)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.scenes.scene2d.Stage.draw(Stage.java:128)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at de.sesu8642.feudaltactics.menu.common.ui.GameScreen.render(GameScreen.java:42)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.Game.render(Game.java:48)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at com.badlogic.gdx.backends.android.AndroidGraphics.onDrawFrame(AndroidGraphics.java:505)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1573)
11-10 20:59:03.691 32570 32607 E AndroidRuntime:    at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1272)

Please select the affected platforms

(others are likely affected too)

tommyettinger commented 2 days ago

I've started to look into this. It's weird. I see 160 bytes used per character in the String, when displayed in a Label. Label does seem to be using more than it should be using, since I compared with TextraTypist's TextraLabel class (which does a lot more out-of-the-box), and even TextraLabel is using about 1/6 the memory of what Label uses. I'm thinking it may boil down to simply being a lot of vertices to store and display, but 6x as many for Label seems odd.

The first thing that comes to mind to resolve this for your purposes would be to put Labels on separate pages (as if they were an actual printed legal document), and only load a Label for the current page at a time.

Frosty-J commented 2 days ago

Pagination, culling off-screen lines (maybe don't even read the whole file in at once), using a library, a native dialogue, opening the licence info in an external app... lots of solutions. In fact, I am certain you have far more licence text than you are legally required to include, but to be honest I'd rather not get into the nitty gritty of it. Just because I can point to endless apps and games that don't display megabytes of licence info no-one will ever read, that's not to say they're in the right for it.

But I do think this should be investigated, see if all the bytes it uses are truly necessary.

Sesu8642 commented 1 day ago

Thanks for looking into this so quickly! Since I already have the license texts organized in a directory structure, I will probably use that to generate a similarly structured UI.

I'm glad you agree that there is an underlying issue that deserves investigating.

tommyettinger commented 1 day ago

So the heap measurement seems too high, and higher than the actual amount of memory used. I tried using a different way, measuring only the Label or TextraLabel, and anything it has as a member variable. The commit where I added this measurement is here. I'm still (surprisingly) seeing more memory used by Label than TextraLabel, but not by much... ThreadMXBean reports 284,068,160 bytes used for Label, or 208,281,760 bytes for TextraLabel, on the same 10,000,000 character string.

TextraLabel I'm more familiar with internally, since I wrote all of it, and I know it needs at least 8 bytes for every displayed glyph (containing 32 bits of per-glyph RGBA8888 color, 16 bits of style information, and 16 bits of UTF-16 char). Taking about 20 bytes per glyph isn't a huge step past that, since this particular piece of text has a lot of lines, and my code has each Line as its own object.

Label I'm less familiar with, but I know it organizes text into runs, and I think each run is its own object. I also think a line break will break a run up. That means a million runs, one per line.

So far, neither of these explains where all 200+ MB are actually going... Thankfully, we now know it isn't more than a gigabyte being used by Label (more than a quarter-gigabyte, but still).