Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve memory usage when no style is applied #659

Closed
dprutean opened this issue Dec 13, 2017 · 7 comments
Closed

Improve memory usage when no style is applied #659

dprutean opened this issue Dec 13, 2017 · 7 comments

Comments

@dprutean
Copy link

dprutean commented Dec 13, 2017

I would like to use the RichTextFx for large SQL files ( can be files from backups ). I tested this and the memory usage is low in the beginning, but increase very fast if I scroll the editor - it later decrease again, meaning there are some temporary large objects created.
I used to test the code from #627
where I disabled the style highlighting.
Could you please check the memory usage for this case ?
My plan is:

  • I will use the RichTextFx to open small or large files. If the file is small I do highlight over the complete document.
  • If the file is large, I will do highlight only for the visible text. Each time the user scrolls I will highlight the visible area ( first wait 1 sec. after each scroll). I will also go back for 100 lines from the current position and look for the first empty line. In SQL files empty lines are in most of the cases spaces between sentences. Then I will highlight everything up to the current position.
    In the same issue on the bottom is also a code snippet for highlighting only the current viewport. This does not currently work if I include it in one of the examples. Could you please provide also the correct code for this ?

I think a similar strategy can be used also by the other cases when large files are loaded.

@JordanMartinez
Copy link
Contributor

I think the best thing to do in this case is to determine how much of a difference in memory occurs between the current approach, where SEG and S are separated from one another, and the previous approach where the two were combined. If it's not much of a difference, this implies a need to optimize something. If there is a difference, I may have introduced a memory leak.

The code snippet was not intended to work out-of-box. It served to give you a general direction to pursue in implementing your own version of the idea.

@dprutean
Copy link
Author

I have started a memory profile session using Netbeans ( is free and has a Java profiler included ). There you can choose to profile memory. Press the delta button for a diff. The first picture is a delta after the project is started, without doing nothing. Many string gets allocated, probably through events.
Second image is after moving the scroll bar. No style was applied during this tests.
Maybe is a good idea to include this class also in your demo. I hope this helps.

richtextfxprofile1
richtextfxprofile2

package org.fxmisc.richtext.demo;


import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;

import java.text.NumberFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ScalabilityDemo extends Application {

    private static final String[] KEYWORDS = new String[] {
            "abstract", "assert", "boolean", "break", "byte",
            "case", "catch", "char", "class", "const",
            "continue", "default", "do", "double", "else",
            "enum", "extends", "final", "finally", "float",
            "for", "goto", "if", "implements", "import",
            "instanceof", "int", "interface", "long", "native",
            "new", "package", "private", "protected", "public",
            "return", "short", "static", "strictfp", "super",
            "switch", "synchronized", "this", "throw", "throws",
            "transient", "try", "void", "volatile", "while"
    };

    private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
    private static final String PAREN_PATTERN = "\\(|\\)";
    private static final String BRACE_PATTERN = "\\{|\\}";
    private static final String BRACKET_PATTERN = "\\[|\\]";
    private static final String SEMICOLON_PATTERN = "\\;";
    private static final String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
    private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";

    private static final Pattern PATTERN = Pattern.compile(
            "(?<KEYWORD>" + KEYWORD_PATTERN + ")"
                    + "|(?<PAREN>" + PAREN_PATTERN + ")"
                    + "|(?<BRACE>" + BRACE_PATTERN + ")"
                    + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
                    + "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")"
                    + "|(?<STRING>" + STRING_PATTERN + ")"
                    + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
    );

    private final int TEST_ROWS = 200000;
    private final Runtime runtime = Runtime.getRuntime();
    private final CodeArea codeArea = new CodeArea();
    private final Label memoryLabel = new Label();

    private Timer updateTimer;

    public static void main(String[] args) {
        //System.setProperty("prism.lcdtext", "false");
        //System.setProperty("prism.text", "t2k");
        launch(args);
    }

    public ScalabilityDemo() {

    }


    private String getSampleCode(){
        StringBuilder sb = new StringBuilder(String.join("\n", new String[] {
            "package com.example;",
            "",
            "import java.util.*;",
            "",
            "public class Foo extends Bar implements Baz {",
            "",
            "    /*",
            "     * multi-line comment",
            "     */",
            "    public static void main(String[] args) {",
            "        // single-line comment",
            "        for(String arg: args) {",
            "            if(arg.length() != 0)",
            "                System.out.println(arg);",
            "            else",
            "                System.err.println(\"Warning: empty string as argument\");",
            "        }",
            "\n"
    }));

        for (int i=0 ; i < TEST_ROWS ; i++) {
            sb.append("        System.out.println(\"");
            sb.append(Math.random());
            sb.append("\");\n");
        }

        sb.append("    }\n");
        sb.append("}\n");
        return sb.toString();
    }

    @Override
    public void start(Stage primaryStage) {
        codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
        codeArea.replaceText(0, 0, getSampleCode());


        Button gcButton = new Button("Garbage Collect");
        gcButton.setOnAction( event -> {
            System.gc ();
            System.runFinalization ();
        });
        Button styleButton = new Button("Parse");
        styleButton.setOnAction( event -> {
            codeArea.richChanges()
                    .filter(ch -> !ch.getInserted().equals(ch.getRemoved()))
                    .subscribe(change -> {
                        codeArea.setStyleSpans(0, computeHighlighting(codeArea.getText()));
                    });
        });
        ToolBar toolbar = new ToolBar();
        toolbar.getItems().addAll( memoryLabel, gcButton, styleButton );

        VBox vBox = new VBox();
        StackPane stackPane = new StackPane(new VirtualizedScrollPane<>(codeArea));
        stackPane.setPrefSize(1200, 880);

        vBox.getChildren().addAll( toolbar, stackPane );
        Scene scene = new Scene(vBox, 1200, 900);
        scene.getStylesheets().add(ScalabilityDemo.class.getResource("java-keywords.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("CodeArea Scalability Test");
        primaryStage.show();

        updateTimer = new Timer(true);
        updateTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                Platform.runLater(() -> {
                    memoryLabel.setText(memoryStatus());
                });
            }
        }, 0, 1000);
    }

    public String memoryStatus() {
        NumberFormat format = NumberFormat.getInstance();
        StringBuilder sb = new StringBuilder();
        long maxMemory = runtime.maxMemory();
        long allocatedMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        sb.append("Used ");
        sb.append(format.format((runtime.totalMemory() - runtime.freeMemory()) / (1024*1024)) );
        sb.append(" MB from ");
        sb.append(format.format((freeMemory + (maxMemory - allocatedMemory)) / (1024*1024)) );
        sb.append(" MB allocated memory. Text length " + codeArea.textProperty().getValue().length());
        return sb.toString();
    }

    private static StyleSpans<Collection<String>> computeHighlighting(String text) {
        Matcher matcher = PATTERN.matcher(text);
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder
                = new StyleSpansBuilder<>();
        while(matcher.find()) {
            String styleClass =
                    matcher.group("KEYWORD") != null ? "keyword" :
                            matcher.group("PAREN") != null ? "paren" :
                                    matcher.group("BRACE") != null ? "brace" :
                                            matcher.group("BRACKET") != null ? "bracket" :
                                                    matcher.group("SEMICOLON") != null ? "semicolon" :
                                                            matcher.group("STRING") != null ? "string" :
                                                                    matcher.group("COMMENT") != null ? "comment" :
                                                                            null; /* never happens */ assert styleClass != null;
            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();
        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }
}

@JordanMartinez
Copy link
Contributor

Thanks for your work. I plan on looking into this more later on today.

@JordanMartinez
Copy link
Contributor

This does not currently work if I include it in one of the examples. Could you also please provide the correct code for this?

Try this:

SuspendableYes allowRichChange = new SuspendableYes();
EventStream<?> plainTextChange = codeArea.richChanges()
        .hook(c -> System.out.println("rich change occurred, firing event"))
        .successionEnds(Duration.ofMillis(500))
        .hook(c -> System.out.println("user has not typed anything for the given duration: firing event"))
        .conditionOn(allowRichChange)
        .hook(c -> System.out.println("this is a valid user-intitiated event, not us changing the style; so allow it to pass through"))
        .filter(ch -> !ch.isIdentity())
        .hook(c -> System.out.println("this is a valid plain text change. firing plaintext change event"));
EventStream<?> dirtyViewport = EventStreams
        .merge(
            codeArea.estimatedScrollXProperty().values(),
            codeArea.estimatedScrollYProperty().values())
        .hook(e -> System.out.println("y or x property value changed"))
        .successionEnds(Duration.ofMillis(200))
        .hook(e -> System.out.println("We've waited long enough, now fire an event"));
EventStreams.merge(plainTextChange, dirtyViewport)
        .hook(c -> System.out.println("either the viewport has changed or a plain text change has occurred. fire an event"))
        .subscribe(dirtyStyles -> {
            // rather than clearing the previously-visible text's styles and setting the current
            // visible text's styles....
            //
            //  codeArea.clearStyle(startOfLastVisibleTextStyle, endOfLastVisibleTextStyle);
            //  codeArea.setStyleSpans(offsetIntoContentBeforeReachingCurrentVisibleStyles, newStyles);
            //
            // do this entire action in one move
            //
            //  codeArea.setStyleSpans(0, styleSpans)
            //
            // where `styleSpans` parameters are...
            //  | unstyled | previously styled | currently visible text | previously styled | unstyled |
            //  | empty list                   | computeHighlighting()  | empty list                   |


            // compute the styles for the currently visible text
            StyleSpans<Collection<String>> visibleTextStyles = computeHighlighting(getVisibleText());

            // calculate how far into the content is the first part of the visible text
            // before modifying the area's styles
            int firstVisibleParIdx = codeArea.visibleParToAllParIndex(0);
            int startOfVisibleStyles = codeArea.position(firstVisibleParIdx, 0).toOffset();
            int lengthFollowingVisibleStyles = codeArea.getLength() - startOfVisibleStyles - visibleTextStyles.length();

            StyleSpans<Collection<String>> styleSpans = visibleTextStyles
                    // 1 single empty list before visible styles
                    .prepend(new StyleSpan<>(Collections.emptyList(), startOfVisibleStyles))
                    // 1 single empty list after visible styles
                    .append(new StyleSpan<>(Collections.emptyList(), lengthFollowingVisibleStyles));

            // no longer allow rich changes as setStyleSpans() will emit a rich change event,
            // which will lead to an infinite loop that will terminate with a StackOverflowError
            allowRichChange.suspendWhile(() ->
                    codeArea.setStyleSpans(0, styleSpans)
            );
        });

@JordanMartinez
Copy link
Contributor

So, using the original rich changes approach used in the demo, it jumps from a styleless ~45 MB on my end to about ~170 MB when the entire area is styled.

When I use the approach I created above, it jumps to about ~100 MB. However, due to FXMisc/Flowless#60, the memory continues to increase due to the scroll values emitting events that trigger a re-computation of the syntax highlighting :-/

@dprutean
Copy link
Author

Could you please provide the complete class source code for the example above ? I am missing the "getVisibleText()" and this does not compile fine for me.

My issue is related only to unstyled document with large files.
Just run the example I provided: 10Mbyte large text jumps memory usage up to 500Mbyte, just by scrolling, without style. Maybe is a way to optimise this.
With large files I am going to style only the visible area.

@JordanMartinez
Copy link
Contributor

My apologies. I posted the same thing in #627, and I included the getVisibleText() there, but not here.

Here's the full source code:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.Paragraph;
import org.fxmisc.richtext.model.StyleSpan;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableNo;
import org.reactfx.SuspendableYes;
import org.reactfx.util.FxTimer;

import java.text.NumberFormat;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ScalabilityDemo extends Application {

    private static final String[] KEYWORDS = new String[] {
            "abstract", "assert", "boolean", "break", "byte",
            "case", "catch", "char", "class", "const",
            "continue", "default", "do", "double", "else",
            "enum", "extends", "final", "finally", "float",
            "for", "goto", "if", "implements", "import",
            "instanceof", "int", "interface", "long", "native",
            "new", "package", "private", "protected", "public",
            "return", "short", "static", "strictfp", "super",
            "switch", "synchronized", "this", "throw", "throws",
            "transient", "try", "void", "volatile", "while"
    };

    private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
    private static final String PAREN_PATTERN = "\\(|\\)";
    private static final String BRACE_PATTERN = "\\{|\\}";
    private static final String BRACKET_PATTERN = "\\[|\\]";
    private static final String SEMICOLON_PATTERN = "\\;";
    private static final String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
    private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";

    private static final Pattern PATTERN = Pattern.compile(
            "(?<KEYWORD>" + KEYWORD_PATTERN + ")"
                    + "|(?<PAREN>" + PAREN_PATTERN + ")"
                    + "|(?<BRACE>" + BRACE_PATTERN + ")"
                    + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
                    + "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")"
                    + "|(?<STRING>" + STRING_PATTERN + ")"
                    + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
    );

    private final int TEST_ROWS = 400 * 100;
    private final Runtime runtime = Runtime.getRuntime();
    private final CodeArea codeArea = new CodeArea();
    private final Label memoryLabel = new Label();

    private Timer updateTimer;

    public static void main(String[] args) {
        System.setProperty("prism.lcdtext", "false");
        System.setProperty("prism.text", "t2k");
        launch(args);
    }

    private String getSampleCode(){
        StringBuilder sb = new StringBuilder(String.join("\n", new String[] {
                "package com.example;",
                "",
                "import java.util.*;",
                "",
                "public class Foo extends Bar implements Baz {",
                "",
                "    /*",
                "     * multi-line comment",
                "     */",
                "    public static void main(String[] args) {",
                "        // single-line comment",
                "        for(String arg: args) {",
                "            if(arg.length() != 0)",
                "                System.out.println(arg);",
                "            else",
                "                System.err.println(\"Warning: empty string as argument\");",
                "        }",
                "\n"
        }));

        for (int i=0 ; i < TEST_ROWS ; i++) {
            sb.append("        System.out.println(\"");
            sb.append(Math.random());
            sb.append("\");\n");
        }

        sb.append("    }\n");
        sb.append("}\n");
        return sb.toString();
    }

    @Override
    public void start(Stage primaryStage) {
        codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
        codeArea.replaceText(0, 0, getSampleCode());


        Button gcButton = new Button("Garbage Collect");
        gcButton.setOnAction( event -> {
            System.gc ();
            System.runFinalization ();
        });
        Button styleButton = new Button("Parse");
        styleButton.setOnAction( event -> {
//            setupOriginal();
//            setupOriginalWithSuccessions();
            setupOptimized();
        });
        styleButton.fire();
        ToolBar toolbar = new ToolBar();
        toolbar.getItems().addAll( memoryLabel, gcButton, styleButton );

        VBox vBox = new VBox();
        StackPane stackPane = new StackPane(new VirtualizedScrollPane<>(codeArea));
        stackPane.setPrefSize(1200, 880);

        vBox.getChildren().addAll( toolbar, stackPane );
        Scene scene = new Scene(vBox, 1200, 900);
        scene.getStylesheets().add(ScalabilityDemo.class.getResource("java-keywords.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("CodeArea Scalability Test");
        primaryStage.show();

        FxTimer.runPeriodically(Duration.ofSeconds(1), () -> memoryLabel.setText(memoryStatus()));
    }

    public void setupOriginal() {
        codeArea.richChanges()
                .filter(ch -> !ch.getInserted().equals(ch.getRemoved()))
                .subscribe(change -> {
                    codeArea.setStyleSpans(0, computeHighlighting(codeArea.getText()));
                });
    }

    public void setupOriginalWithSuccessions() {
        codeArea.richChanges()
                .successionEnds(Duration.ofMillis(500))
                .filter(ch -> !ch.getInserted().equals(ch.getRemoved()))
                .subscribe(change -> {
                    codeArea.setStyleSpans(0, computeHighlighting(codeArea.getText()));
                });
    }

    public void setupOptimized() {
        SuspendableYes allowRichChange = new SuspendableYes();
        EventStream<?> plainTextChange = codeArea.richChanges()
                .hook(c -> System.out.println("rich change occurred, firing event"))
                .successionEnds(Duration.ofMillis(500))
                .hook(c -> System.out.println("user has not typed anything for the given duration: firing event"))
                .conditionOn(allowRichChange)
                .hook(c -> System.out.println("this is a valid user-intitiated event, not us changing the style; so allow it to pass through"))
                .filter(ch -> !ch.isIdentity())
                .hook(c -> System.out.println("this is a valid plain text change. firing plaintext change event"));
        EventStream<?> dirtyViewport = EventStreams
                .merge(
                        codeArea.estimatedScrollXProperty().values(),
                        codeArea.estimatedScrollYProperty().values())
                .hook(e -> System.out.println("y or x property value changed"))
                .successionEnds(Duration.ofMillis(200))
                .hook(e -> System.out.println("We've waited long enough, now fire an event"));
        EventStreams.merge(plainTextChange, dirtyViewport)
                .hook(c -> System.out.println("either the viewport has changed or a plain text change has occurred. fire an event"))
                .subscribe(dirtyStyles -> {
                    // rather than clearing the previously-visible text's styles and setting the current
                    // visible text's styles....
                    //
                    //  codeArea.clearStyle(startOfLastVisibleTextStyle, endOfLastVisibleTextStyle);
                    //  codeArea.setStyleSpans(offsetIntoContentBeforeReachingCurrentVisibleStyles, newStyles);
                    //
                    // do this entire action in one move
                    //
                    //  codeArea.setStyleSpans(0, styleSpans)
                    //
                    // where `styleSpans` parameters are...
                    //  | unstyled | previously styled | currently visible text | previously styled | unstyled |
                    //  | empty list                   | computeHighlighting()  | empty list                   |


                    // compute the styles for the currently visible text
                    StyleSpans<Collection<String>> visibleTextStyles = computeHighlighting(getVisibleText());

                    // calculate how far into the content is the first part of the visible text
                    // before modifying the area's styles
                    int firstVisibleParIdx = codeArea.visibleParToAllParIndex(0);
                    int startOfVisibleStyles = codeArea.position(firstVisibleParIdx, 0).toOffset();
                    int lengthFollowingVisibleStyles = codeArea.getLength() - startOfVisibleStyles - visibleTextStyles.length();

                    StyleSpans<Collection<String>> styleSpans = visibleTextStyles
                            // 1 single empty list before visible styles
                            .prepend(new StyleSpan<>(Collections.emptyList(), startOfVisibleStyles))
                            // 1 single empty list after visible styles
                            .append(new StyleSpan<>(Collections.emptyList(), lengthFollowingVisibleStyles));

                    // no longer allow rich changes as setStyleSpans() will emit a rich change event,
                    // which will lead to an infinite loop that will terminate with a StackOverflowError
                    allowRichChange.suspendWhile(() ->
                            codeArea.setStyleSpans(0, styleSpans)
                    );
                });
    }

    public String getVisibleText() {
        return codeArea.getVisibleParagraphs().map(Paragraph::getText).reduce((a, b) -> a + "\n" + b).getOrElse("");
    }

    public String memoryStatus() {
        NumberFormat format = NumberFormat.getInstance();
        StringBuilder sb = new StringBuilder();
        long maxMemory = runtime.maxMemory();
        long allocatedMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        sb.append("Used ");
        sb.append(format.format((runtime.totalMemory() - runtime.freeMemory()) / (1024*1024)) );
        sb.append(" MB from ");
        sb.append(format.format((freeMemory + (maxMemory - allocatedMemory)) / (1024*1024)) );
        sb.append(" MB allocated memory. Text length " + codeArea.textProperty().getValue().length());
        return sb.toString();
    }

    private static StyleSpans<Collection<String>> computeHighlighting(String text) {
        Matcher matcher = PATTERN.matcher(text);
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder
                = new StyleSpansBuilder<>();
        while(matcher.find()) {
            String styleClass =
                    matcher.group("KEYWORD") != null ? "keyword" :
                            matcher.group("PAREN") != null ? "paren" :
                                    matcher.group("BRACE") != null ? "brace" :
                                            matcher.group("BRACKET") != null ? "bracket" :
                                                    matcher.group("SEMICOLON") != null ? "semicolon" :
                                                            matcher.group("STRING") != null ? "string" :
                                                                    matcher.group("COMMENT") != null ? "comment" :
                                                                            null; /* never happens */ assert styleClass != null;
            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();
        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants