SpreadSheetView Not Being Garbage Collected?

102 views
Skip to first unread message

Jim Bourbeau

unread,
Oct 30, 2020, 10:09:06 AM10/30/20
to ControlsFX
Hello everyone,

I am using SpreadSheetView in a desktop application and have noticed that my heap size grows whenever I replace a SpreadSheetView object with a new one. Essentially I have

1. created several objects each containing 4 spreadsheetview fields
2. from the main class, I retrieve the 4 fields from one of these objects and place them in a tabPane. The heap grows at this point, which I think would be expected.
3. I then replace the spreadsheetviews in the tabpane with a new set of 4 spreadsheetviews retrieved from a different object. The heap grows further here and does not lessen even after garbage collection even though I have replaced the 4 original spvs with new ones (I would have thought the old ones would be garbage collected)

Below is the code for a sample demonstrating this issue.

My question is, does anyone have any thoughts on how to send the old, no longer shown, spvs to the garbage collection?

Thanks so much for any thoughts!

Jim

heapSize.jpg
1. Notice the heap after the first garbage collection is roughly 89,000,000
2. After loading the first course, heap is 126,000,000 after GC
3. After selecting the first course, replacing with the second, and replacing that with the third and then fourth course, the heap after GC is now 242,000,000. I would like it to be at least 126,000,000 which was what it was after the first load.

public class Main extends Application {


//make 4 tabs
Tab tab1 = new Tab("Quarter 1");
Tab tab2 = new Tab("Quarter 2");
Tab tab3 = new Tab("Quarter 3");
Tab tab4 = new Tab("Quarter 4");

//make a tabPane to hold the tabs
TabPane tabPane = new TabPane();

//create some course fields
Course courseA;
Course courseB;
Course courseC;
Course courseD;


@Override
public void start(Stage primaryStage) throws Exception {

primaryStage.setTitle("Hello World");

//add the tabs to the tabpane
tabPane.getTabs().addAll(tab1, tab2, tab3, tab4);

//make a title, and some buttons to call the different course spreadsheetviews (spvs)
Label title = new Label("Title Here");
Button button1 = new Button("course A");
Button button2 = new Button("course B");
Button button3 = new Button("course C");
Button button4 = new Button("course D");

//add events for the buttons
makeEvent(button1, 1);
makeEvent(button2, 2);
makeEvent(button3, 3);
makeEvent(button4, 4);

//place the buttons in a menubar
ButtonBar buttonBar = new ButtonBar();
buttonBar.getButtons().addAll(button1, button2, button3, button4);

//place the menubar and the title in a vbox
VBox rootVbox = new VBox();
rootVbox.getChildren().addAll(buttonBar, title, tabPane);

//we will now initialize the courses which will build their spreadsheetview object fields, so they will be ready and waiting in memory.
//this is not necessary for the this small program but in larger version, this is done on program startup to speed up performance later
//I will then call for these spreadsheet fieds when I press the appropriate button
courseA = new Course("A");
courseB = new Course("B");
courseC = new Course("C");
courseD = new Course("D");



//set the primary stage and show
primaryStage.setScene(new Scene(rootVbox, 600, 600));
primaryStage.show();

}

public static void main(String[] args) {
launch(args);
}

private void makeEvent(Button button, int spvNumber) {

button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
switch (spvNumber) {
case 1:
tab1.setContent(courseA.getSpv(1));
tab2.setContent(courseA.getSpv(2));
tab3.setContent(courseA.getSpv(3));
tab4.setContent(courseA.getSpv(4));
tabPane.requestLayout(); //I need to place this in order for the first spv to show on startup. . . ?
break;
case 2:
tab1.setContent(courseB.getSpv(1));
tab2.setContent(courseB.getSpv(2));
tab3.setContent(courseB.getSpv(3));
tab4.setContent(courseB.getSpv(4));
tabPane.requestLayout();
break;
case 3:
tab1.setContent(courseC.getSpv(1));
tab2.setContent(courseC.getSpv(2));
tab3.setContent(courseC.getSpv(3));
tab4.setContent(courseC.getSpv(4));
tabPane.requestLayout();
break;
case 4:
tab1.setContent(courseD.getSpv(1));
tab2.setContent(courseD.getSpv(2));
tab3.setContent(courseD.getSpv(3));
tab4.setContent(courseD.getSpv(4));
tabPane.requestLayout();
break;
default:
break;
}
}
});
}
public class Course {

private SpreadsheetView quarter1;
private SpreadsheetView quarter2;
private SpreadsheetView quarter3;
private SpreadsheetView quarter4;


String name;

public Course(String value) {
this.name = value;
quarter1 = createSpv("1");
quarter2 = createSpv("2");
quarter3 = createSpv("3");
quarter4 = createSpv("4");

}

public SpreadsheetView getSpv(int quarter) {
switch (quarter){
case 1:
quarter1 = createSpv("1");
return quarter1;
case 2:
quarter2 = createSpv("2");
return quarter2;
case 3:
quarter3 = createSpv("3");
return quarter3;
case 4:
quarter4 = createSpv("4");
return quarter4;
default:
return null;
}
}


private SpreadsheetView createSpv(String quarter){

SpreadsheetView spv = new SpreadsheetView(createGrid(quarter));
return spv;
}


private GridBase createGrid(String quarter){
GridBase mainGrid = new GridBase(50, 100);
mainGrid.setRows(createRows(quarter));
return mainGrid;
}

private ObservableList<ObservableList<SpreadsheetCell>> createRows(String quarter){
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();;

for (int row = 0; row < 50; ++row) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
for (int column = 0; column < 100; ++column) {
list.add(SpreadsheetCellType.STRING.createCell(row, column, 1, 1, name + " " + quarter));
}
rows.add(list);
}
return rows;
}
}

Samir Hadzic

unread,
Oct 31, 2020, 8:06:51 AM10/31/20
to ControlsFX
Hello,

That's odd.

I see that you are using Java Visual VM, could you maybe try to see why these "old" SpreadsheetView are not being garbage collected? To see if maybe an object inside the SPreadsheetView is linked to a GC root and prevent the garbage collection?

I could take a closer look on Monday.

Regards,

Jim Bourbeau

unread,
Nov 1, 2020, 12:34:31 PM11/1/20
to ControlsFX
Samir Hadzic,

Thank you for responding! I have been trying to dig into the heap using Visual VM to determine a reason. Unfortunately, I am not that experienced with garbage collection and am learning a lot as I go. I am not seeing any reason for it to be not garbage collected, but it is likely I may be missing something. Thanks for offering to look into it as well. I would really appreciate anything thought on what you find.


--
You received this message because you are subscribed to the Google Groups "ControlsFX" group.
To unsubscribe from this group and stop receiving emails from it, send an email to controlsfx-de...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/controlsfx-dev/b9882ad1-0a8c-46d5-83e9-03ee611c54dan%40googlegroups.com.

Sam'

unread,
Nov 2, 2020, 4:30:30 AM11/2/20
to control...@googlegroups.com
Hi,

Regarding the profiling process, you need to click on the
image.png

button. Open the heap dump, select "Objects".

Then filter by "SpreadsheetView" at the bottom, and you will find the 16 SpreadsheetView, corresponding to the 4 courses :
image.png



If you then click on "GC root", you will see why these objects are being retained:
image.png


Here, it's pretty simple. When you click on a "Course" button, you create 4 SpreadsheetViews that you set on the "Quarter" of the "Course" object. And each "Course" object is held by the Main classes. Therefore you have at most, 16 SpreadsheetView loaded in memory. If you want to fix this, you have to release them. Something like that for example:

 private void makeEvent(Button button, int spvNumber) {

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                courseA.clean();
                courseB.clean();
                courseC.clean();
                courseD.clean();
                switch (spvNumber) {


And in Course :
 public void clean(){
        quarter1 = null;
        quarter2 = null;
        quarter3 = null;
        quarter4 = null;
    }

Jim Bourbeau

unread,
Nov 2, 2020, 8:22:51 PM11/2/20
to ControlsFX
Hi,

Thanks for the very detailed reply and explanation! I do have a question still. First though, I have to apologize, I had been experimenting with the sample code before I pasted it into the post and forgot to remove the code in the "Course" class in the "getSpv" method that builds a new spreadsheet each time the method is called by the button events. The getter was supposed to simply return the quarter SpreadSheetView that was placed in memory when the Course was constructed (see the constructor method). The idea would be to

1.) Create the Course objects when the application starts. Thus each Course will have its four quarter objects built and in memory due to the constructor method
2.) Make a call to a Course getter to get the quarter SpreadSheetView objects and place them in the TabPane.
3.) Make a call to a different course and get its SpreadSheetView objects and replace the current ones in the TabPane without clearing them from the Course object.

Using Java Visual VM, I can see that the quarter objects are present in memory when the program starts. I then get and place CourseA's quarter objects into the TabPane (again, this is done WITHOUT creating a new SpreadSheetView) and the heap size increases. Replacing CourseA with CourseB quarters increases the heap size even more even after garbage collection. This is the part I don't understand. It seems that all the quarters are in memory at the startup, and the number of quarters in memory never changes throughout the running of the application, and yet the heap size increases until all courses have been placed into the TabPane once. Why would this be if they are already in memory?  

Thanks again!

Samir Hadzic

unread,
Nov 3, 2020, 8:15:49 AM11/3/20
to ControlsFX
Hi,

Please update the source code accordingly so that I can test it easily (don't forget to also paste the imports).

When the SpreadsheetView is built, only the model is created. When you place one in the TabPane, you have a bunch of cells from the view part that are created. The SpreadsheetView is actually being rendered. So that doesn't shock me too much.

Jim Bourbeau

unread,
Nov 4, 2020, 9:54:50 AM11/4/20
to ControlsFX
Hi,

Here is the updated code. The idea that the spreadsheet objects are only being rendered and need to create their visual components when being displayed does make a lot of sense. I was hoping there would be a way to cut down on the memory expense since in the real application there are far more spreadsheetview objects and it gets expensive. I know I could clear them and rebuild them as needed, but that just strikes me as so inefficient and thus slower, since I would be rebuilding a large spreadsheetview object repeatedly during the running of the application. Thanks so much for your thoughts on this! You have been great.

Main Class:
package sample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
Course Class:
package sample;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.controlsfx.control.spreadsheet.*;

public class Course {

private SpreadsheetView quarter1;
private SpreadsheetView quarter2;
private SpreadsheetView quarter3;
private SpreadsheetView quarter4;


String name;

public Course(String value) {
this.name = value;
quarter1 = createSpv("1");
quarter2 = createSpv("2");
quarter3 = createSpv("3");
quarter4 = createSpv("4");

}

public SpreadsheetView getSpv(int quarter) {
switch (quarter){
case 1:
                return quarter1;
case 2:
return quarter2;
case 3:
return quarter3;
case 4:

return quarter4;
default:
return null;
}
}

private SpreadsheetView createSpv(String quarter){

        return new SpreadsheetView(createGrid(quarter));

Sam'

unread,
Nov 6, 2020, 12:37:46 PM11/6/20
to control...@googlegroups.com
Hello,

Allright I've been investigating a bit. When the SpreadsheetView is displayed, it creates a bunch a visual elements (created by the TableView itself underneath). When the SpreadsheetView is not longer shown in a Tab, but kept. These objects are not destroyed. I wonder if the same scenario is happening if you replace the SpreadsheetView with a TableView?

I do not know how it would be possible to release all the created components. So that explains why it's always growing when you are displaying a new SpreadsheetView. Each time you display a new SpreadsheetView, the TableView underneath creates a new VirtualFlow, new cells etc that are somehow never garbage collected.

So if your application is doing that, you will get into trouble.

We have two choices:
- Confirm that this behavior is not happening with the TableView itself and try to fix it for the SpreadsheetView
- Only creates the needed SpreadsheetView.

For solution 2, this is what I do in my applications and how I intended the SpreadsheetView to be used. You should create only one SpreadsheetView (the one being seen) or maybe 4 at most. And simply set the new grid corresponding to your course when you want.
That would be something like that:

package org.controlsfx;



import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.controlsfx.control.spreadsheet.*;

public class Course {

    private GridBase quarter1;
    private GridBase quarter2;
    private GridBase quarter3;
    private GridBase quarter4;


    String name;

    public Course(String value) {
        this.name = value;
        quarter1 = createGrid("1");
        quarter2 = createGrid("2");
        quarter3 = createGrid("3");
        quarter4 = createGrid("4");

    }

    public GridBase getSpv(int quarter) {

And

package org.controlsfx;


import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.spreadsheet.SpreadsheetView;
                if (tab1.getContent() == null) {
                    tab1.setContent(new SpreadsheetView());
                    tab2.setContent(new SpreadsheetView());
                    tab3.setContent(new SpreadsheetView());
                    tab4.setContent(new SpreadsheetView());
                }
                switch (spvNumber) {
                    case 1:
                        ((SpreadsheetView) tab1.getContent()).setGrid(courseA.getSpv(1));
                        ((SpreadsheetView) tab2.getContent()).setGrid(courseA.getSpv(2));
                        ((SpreadsheetView) tab3.getContent()).setGrid(courseA.getSpv(3));
                        ((SpreadsheetView) tab4.getContent()).setGrid(courseA.getSpv(4));

                        tabPane.requestLayout(); //I need to place this in order for the first spv to show on startup. . . ?
                        break;
                    case 2:
                        ((SpreadsheetView) tab1.getContent()).setGrid(courseB.getSpv(1));
                        ((SpreadsheetView) tab2.getContent()).setGrid(courseB.getSpv(2));
                        ((SpreadsheetView) tab3.getContent()).setGrid(courseB.getSpv(3));
                        ((SpreadsheetView) tab4.getContent()).setGrid(courseB.getSpv(4));
                        tabPane.requestLayout();
                        break;
                    case 3:
                        ((SpreadsheetView) tab1.getContent()).setGrid(courseC.getSpv(1));
                        ((SpreadsheetView) tab2.getContent()).setGrid(courseC.getSpv(2));
                        ((SpreadsheetView) tab3.getContent()).setGrid(courseC.getSpv(3));
                        ((SpreadsheetView) tab4.getContent()).setGrid(courseC.getSpv(4));
                        tabPane.requestLayout();
                        break;
                    case 4:
                        ((SpreadsheetView) tab1.getContent()).setGrid(courseD.getSpv(1));
                        ((SpreadsheetView) tab2.getContent()).setGrid(courseD.getSpv(2));
                        ((SpreadsheetView) tab3.getContent()).setGrid(courseD.getSpv(3));
                        ((SpreadsheetView) tab4.getContent()).setGrid(courseD.getSpv(4));

                        tabPane.requestLayout();
                        break;
                    default:
                        break;
                }
            }
        });
    }

}

By doing so, you'll be reusing the SpreadsheetView and avoid creating unnecessary objects.  Yes it will be a bit slower since the SpreadsheetView will need to be updated. But if we go for solution 1 and we manage to garbage collect what we want, it'll also be slower.

For the record, if you do use my solution, it seems that we have a memory leak regarding the ContextMenu on SpreadsheetColumn that I do not explain. Apparently the "fixItem" menuItem is retaining something and all the ContextMenu created are piling up.

I hope it's a bit clearer for you. Let me know how this can fit in your application and what the next steps would be.

Regards,

Jim Bourbeau

unread,
Nov 10, 2020, 7:34:48 AM11/10/20
to ControlsFX
Hi,

Sorry for the delay. I have been occupied for the last few days.

Based on your suggestions, I think option 2 is the better path to take for my application. However, I implemented your code suggestions and did some analysis on it using VisualVM. The good news is that it does limit the number of spreadsheetviews to 4 and this does not change while interacting with the application on runtime. However, the heap size does continue to grow more with every interaction (clicking on the course buttons) and does not reduce back to its expected heap size after GC. In fact, this appears to never peak and eventually the heap gets too big (try just clicking the same button over and over again). I have no idea why this might be as we are not using any new objects. I checked the GridBase objects, and these remain at a count of 16 which makes sense since there are 4 courses with 4 quarters (gridBases) each. Does this make sense? Does this have to do with the memory leak you referred to?
image.png

Another approach that seems to work better (at least initially) is to place the SpreadsheetVeiw objects in the course object. When retrieving them from the main class, the course object sets them to null, then completely rebuilds the spreadsheetview object before sending it to the main class. I am trying this out and it seems to work a little better, but I have not fully implemented it yet.

Sam'

unread,
Nov 12, 2020, 11:57:49 AM11/12/20
to control...@googlegroups.com
Hi,

I think we have a memory leak on the SpreadsheetColumns that make them being retained and grows.
Which version of ControlsFX are you using? Are you able to build it with a custom code?
If you replace the SpreadsheetColumn code to this one :
/**
 * Copyright (c) 2013, 2016 ControlsFX
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *     * Neither the name of ControlsFX, any associated website, nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.controlsfx.control.spreadsheet;

import static impl.org.controlsfx.i18n.Localization.asKey;
import static impl.org.controlsfx.i18n.Localization.localize;
import impl.org.controlsfx.spreadsheet.CellView;
import impl.org.controlsfx.spreadsheet.CellViewSkin;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.WindowEvent;
import org.controlsfx.tools.Utils;

/**
 * A {@link SpreadsheetView} is made up of a number of {@link SpreadsheetColumn}
 * instances.
 *
 * <h3>Configuration</h3> SpreadsheetColumns are instantiated by the
 * {@link SpreadsheetView} itself, so there is no public constructor for this
 * class. To access the available columns, you need to call
 * {@link SpreadsheetView#getColumns()}.
 *
 * <p>
 * SpreadsheetColumn gives you the ability to modify some aspects of the column,
 * for example the {@link #setPrefWidth(double) width} or
 * {@link #setResizable(boolean) resizability} of the column.
 *
 * <p>
 * You have the ability to freeze this column at the left of the SpreadsheetView
 * by calling {@link #setFixed(boolean)}. But you are strongly advised to check
 * if it is possible with {@link #isColumnFixable()} before calling
 * {@link #setFixed(boolean)}. Take a look at the {@link SpreadsheetView}
 * description to understand the freezing constraints.
 *
 * <p>
 * If the column can be frozen, a {@link ContextMenu} will appear if the user
 * right-clicks on it. If not, nothing will appear and the user will not have
 * the possibility to freeze it.
 *
 * <h3>Screenshot</h3>
 * The column <b>A</b> is frozen and is covering column <b>B</b> and partially
 * column <b>C</b>. The context menu is being shown and offers the possibility
 * to unfreeze the column.
 *
 * <br>
 * <br>
 * <center><img src="fixedColumn.png" alt="Screenshot of SpreadsheetColumn"></center>
 *
 * @see SpreadsheetView
 */
public final class SpreadsheetColumn {

    /**
     * *************************************************************************
     * * Private Fields * *
     * ************************************************************************
     */
    private final SpreadsheetView spreadsheetView;
    final TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> column;
    private final boolean canFix;
    private final Integer indexColumn;
    private MenuItem fixItem;
    //The current filter applied on this column if any.
    private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();

    /**
     * *************************************************************************
     * * Constructor * *
     * ************************************************************************
     */
    /**
     * Creates a new SpreadsheetColumn.
     *
     * @param column
     * @param spreadsheetView
     * @param indexColumn
     */
    SpreadsheetColumn(final TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> column,
            final SpreadsheetView spreadsheetView, final Integer indexColumn, Grid grid) {
        this.spreadsheetView = spreadsheetView;
        this.column = column;
        column.setMinWidth(0);
        this.indexColumn = indexColumn;
        canFix = initCanFix(grid);

        // The contextMenu creation must be on the JFX thread
        CellView.getValue(() -> {
            column.setContextMenu(getColumnContextMenu());
        });

        // When changing frozen fixed columns, we need to update the ContextMenu.
        spreadsheetView.fixingColumnsAllowedProperty().addListener(weakFixingColumnListener);

        // When ColumnsHeaders are changing, we update the text
        grid.getColumnHeaders().addListener(weakColumnListener);

        // When changing rows, we re-calculate if this columns can be fixed.
        grid.getRows().addListener(weakRowsListener);

        filterProperty.addListener(new ChangeListener<Filter>() {
            @Override
            public void changed(ObservableValue<? extends Filter> observable, Filter oldFilter, Filter newFilter) {
                if (newFilter != null) {
                    //We verify this cell can actually be filtered.
                    //FIXME Only one row can be filtered.
                    if (spreadsheetView.getFilteredRow() == -1) {
                        setFilter(null);
                        return;
                    }
                    SpreadsheetCell cell = spreadsheetView.getGrid().getRows().get(spreadsheetView.getFilteredRow()).get(indexColumn);
                    if (cell.getColumnSpan() > 1) {
                        setFilter(null);
                        return;
                    }
                }
                Event.fireEvent(column, new Event(CellViewSkin.FILTER_EVENT_TYPE));
            }
        });
    }

    private InvalidationListener columnHeaderListener = new InvalidationListener() {
        @Override
        public void invalidated(Observable arg0) {
            List<String> columnsHeader = spreadsheetView.getGrid().getColumnHeaders();
            if (columnsHeader.size() <= indexColumn) {
                setText(Utils.getExcelLetterFromNumber(indexColumn));
            } else if (!columnsHeader.get(indexColumn).equals(getText())) {
                setText(columnsHeader.get(indexColumn));
            }
        }
    };
    private WeakInvalidationListener weakColumnListener = new WeakInvalidationListener(columnHeaderListener);

    private InvalidationListener rowsListener = new InvalidationListener() {
        @Override
        public void invalidated(Observable arg0) {
            initCanFix(spreadsheetView.getGrid());
        }
    };
    private WeakInvalidationListener weakRowsListener = new WeakInvalidationListener(rowsListener);

    private ChangeListener<Boolean> fixingColumnListener = new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
            CellView.getValue(() -> {
                column.setContextMenu(getColumnContextMenu());
            });
        }
    };
    private WeakChangeListener<Boolean> weakFixingColumnListener = new WeakChangeListener<>(fixingColumnListener);

    /**
     * *************************************************************************
     * * Public Methods * *
     * ************************************************************************
     */
    /**
     * Return whether this column is frozen or not.
     *
     * @return true if this column is frozen.
     */
    public boolean isFixed() {
        return spreadsheetView.getFixedColumns().contains(this);
    }

    /**
     * Freeze this column to the left if possible, although it is recommended
     * that you call {@link #isColumnFixable()} before trying to freeze a
     * column.
     *
     * If you want to freeze several columns (because of a span for example),
     * add all the columns directly in {@link SpreadsheetView#getFixedColumns()
     * }. Always use {@link SpreadsheetView#areSpreadsheetColumnsFixable(java.util.List)
     * } before.
     *
     * @param fixed
     */
    public void setFixed(boolean fixed) {
        if (fixed) {
            spreadsheetView.getFixedColumns().add(this);
        } else {
            spreadsheetView.getFixedColumns().removeAll(this);
        }
    }

    /**
     * Set the width of this column.
     *
     * @param width
     */
    public void setPrefWidth(double width) {
        width = Math.ceil(width);
        if (column.getPrefWidth() == width && column.getWidth() != width) {
            column.impl_setWidth(width);
        } else {
            column.setPrefWidth(width);
        }
        spreadsheetView.columnWidthSet(indexColumn);
    }

    /**
     * Return the actual width of the column.
     *
     * @return the actual width of the column
     */
    public double getWidth() {
        return column.getWidth();
    }

    /**
     * Return the Property related to the actual width of the column.
     *
     * @return the Property related to the actual width of the column.
     */
    public final ReadOnlyDoubleProperty widthProperty() {
        return column.widthProperty();
    }

    /**
     * Set the minimum width for this SpreadsheetColumn.
     *
     * @param value
     */
    public final void setMinWidth(double value) {
        column.setMinWidth(value);
    }

    /**
     * Return the minimum width for this SpreadsheetColumn.
     *
     * @return the minimum width for this SpreadsheetColumn.
     */
    public final double getMinWidth() {
        return column.getMinWidth();
    }

    /**
     * Return the Property related to the minimum width of this
     * SpreadsheetColumn.
     *
     * @return the Property related to the minimum width of this
     * SpreadsheetColumn.
     */
    public final DoubleProperty minWidthProperty() {
        return column.minWidthProperty();
    }

    /**
     * Return the Property related to the maximum width of this
     * SpreadsheetColumn.
     *
     * @return the Property related to the maximum width of this
     * SpreadsheetColumn.
     */
    public final DoubleProperty maxWidthProperty() {
        return column.maxWidthProperty();
    }

    /**
     * Set the maximum width for this SpreadsheetColumn.
     *
     * @param value
     */
    public final void setMaxWidth(double value) {
        column.setMaxWidth(value);
    }

    /**
     * Return the maximum width for this SpreadsheetColumn.
     *
     * @return the maximum width for this SpreadsheetColumn.
     */
    public final double getMaxWidth() {
        return column.getMaxWidth();
    }

    /**
     * If this column can be resized by the user
     *
     * @param b
     */
    public void setResizable(boolean b) {
        column.setResizable(b);
    }

    /**
     * If the column is resizable, it will compute the optimum width for all the
     * visible cells to be visible.
     */
    public void fitColumn() {
        if (column.isResizable() && spreadsheetView.getCellsViewSkin() != null) {
            spreadsheetView.getCellsViewSkin().resize(column, 100);
        }
    }

    /**
     * Indicate whether this column can be frozen or not. Call that method
     * before calling {@link #setFixed(boolean)} or adding an item to
     * {@link SpreadsheetView#getFixedColumns()}.
     *
     * A column cannot be frozen alone if any cell inside the column has a
     * column span superior to one.
     *
     * @return true if this column is freezable.
     */
    public boolean isColumnFixable() {
        return canFix && spreadsheetView.isFixingColumnsAllowed();
    }

    public void setFilter(Filter filter) {
        this.filterProperty.setValue(filter);
    }

    public Filter getFilter() {
        return filterProperty.get();
    }

    public ObjectProperty filterProperty() {
        return filterProperty;
    }

    /**
     * *************************************************************************
     * * Private Methods * *
     * ************************************************************************
     */
    private void setText(String text) {
        column.setText(text);
    }

    private String getText() {
        return column.getText();
    }

    /**
     * Generate a context Menu in order to freeze/unfreeze some column It is
     * shown when right-clicking on the column header
     *
     * @return a context menu.
     */
    private ContextMenu getColumnContextMenu() {
        if (isColumnFixable()) {
            final ContextMenu contextMenu = new ContextMenu();

            this.fixItem = new MenuItem(localize(asKey("spreadsheet.column.menu.fix"))); //$NON-NLS-1$
            contextMenu.setOnShowing(new EventHandler<WindowEvent>() {

                @Override
                public void handle(WindowEvent event) {
                    if (!isFixed()) {
                        fixItem.setText(localize(asKey("spreadsheet.column.menu.fix"))); //$NON-NLS-1$
                    } else {
                        fixItem.setText(localize(asKey("spreadsheet.column.menu.unfix"))); //$NON-NLS-1$
                    }
                }
            });
            fixItem.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("pinSpreadsheetView.png")))); //$NON-NLS-1$
            fixItem.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent arg0) {
                    if (!isFixed()) {
                        setFixed(true);
                    } else {
                        setFixed(false);
                    }
                }
            });
            contextMenu.getItems().addAll(fixItem);

            return contextMenu;
        } else {
            return new ContextMenu();
        }
    }

    /**
     * Verify that you can freeze this column.
     *
     * @return if it's freezable.
     */
    private boolean initCanFix(Grid grid) {
        for (ObservableList<SpreadsheetCell> row : grid.getRows()) {
            int columnSpan = row.get(indexColumn).getColumnSpan();
            if (columnSpan > 1) {
                return false;
            }
        }
        return true;
    }
}

You should no longer see the heap grows. I can provide the custom jar if you have trouble building it.

What is the second solution ? You are in fact re-creating a new SpreadsheetView each time? In that case, it will indeed fix the memory leak problem since the whole SpreadsheetView is being trashed. But it could be a bit slower because you'll have to recreate the whole SpreadsheetView.

Let me know,


Jim Bourbeau

unread,
Nov 27, 2020, 7:56:53 PM11/27/20
to ControlsFX
Hi Sam, 

Sorry it has taken me so long to respond. After playing with several scenarios, I have concluded the best solution for me is to trash and rebuild the spreadsheet view every time. The sacrifice in speed in reality turned out to be less than I thought and the decrease in memory use was huge. My application went from using over 1300 MB to less than 50MB by simply do this. Thanks for all your input on this! I have learned some things from you, and really appreciate it!

Happy Thanksgiving! 

Jim

Reply all
Reply to author
Forward
0 new messages