mabe02 / lanterna

Java library for creating text-based GUIs
GNU Lesser General Public License v3.0
2.23k stars 243 forks source link

Avoid unnecessary draw calls for invisible components in a Panel (Container) #552

Closed Chris-GW closed 2 years ago

Chris-GW commented 2 years ago

Issue this pull requests tries to fix

When using Lanterna I noticed a quite high CPU usage whenever I had some Components invisible. After some debugging I could find the problem in the Panel isInvalid() method. This checks if one of its children components is invalid. If this is the case, all components are redrawn, skipping the invisible ones. This way the invisible components will not be drawn and will not be set to valid again. In the next isInvalid check, the invisible components cause the Panel to be redrawn again.

Implemented soultion

Ignore invisible components during Panel.isInvalid() check.

@Override
public boolean isInvalid() {
    synchronized(components) {
        for(Component component: components) {
            // a Panel (Container) isInvalid when any child component isInvalid AND also isVisible
            if(component.isInvalid() && component.isVisible()) { 
                return true;
            }
        }
    }
    return super.isInvalid() || layoutManager.hasChanged();
}

Demonstration before

Panel without this fix

Demonstration after

Fixed Panel with isInvalid AND isVisible

Used code in shown demonstration

import com.googlecode.lanterna.TextColor.ANSI;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

public class DemonstrationPanel extends Panel {

    AtomicInteger drawCounter = new AtomicInteger();
    Label drawCounterLabel = new Label("0").setLabelWidth(6);

    Label spinnerLabel = new Label("");
    AnimatedLabel spinner = AnimatedLabel.createClassicSpinningLine(500);
    Button toggleVisibleBtn = new Button("toggle spinner visibility", this::toggleSpinningLineVisibility);

    public DemonstrationPanel() {
        setLayoutManager(new GridLayout(2));

        addComponent(new Label("Draw calls: "));
        addComponent(drawCounterLabel);

        addComponent(spinnerLabel);
        addComponent(spinner);
        addComponent(toggleVisibleBtn, GridLayout.createHorizontallyFilledLayoutData(2));
    }

    private void toggleSpinningLineVisibility() {
        spinner.setVisible(!spinner.isVisible());
    }

    @Override
    protected void onBeforeDrawing() {
        int drawCounter = this.drawCounter.incrementAndGet();
        drawCounterLabel.setText(String.valueOf(drawCounter));
        spinnerLabel.setText("Visibility [" + spinner.isVisible() + "]");
    }

    public static void main(String[] args) throws IOException {
        try (Terminal terminal = new DefaultTerminalFactory().createTerminal()) {
            TerminalScreen screen = new TerminalScreen(terminal);
            screen.startScreen();

            BasicWindow window = new BasicWindow();
            window.setComponent(new DemonstrationPanel());

            MultiWindowTextGUI gui = new MultiWindowTextGUI(screen);
            gui.addWindowAndWait(window);
        }
    }

}
mabe02 commented 2 years ago

Nice catch, thank you!