jfx icon indicating copy to clipboard operation
jfx copied to clipboard

8185887: TableRowSkinBase fails to correctly virtualize cells in horizontal direction

Open yososs opened this issue 5 years ago • 23 comments

If there are many columns, the current TableView will stall scrolling. Resolving this performance issue requires column virtualization. Virtualization mode is enabled when the row height is fixed by the following method.

tableView.setFixedCellSize(height)

This proposal includes a fix because the current code does not correctly implement column virtualization.

The improvement of this proposal can be seen in the following test program.

import java.util.Arrays;
import java.util.Collections;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class BigTableViewTest2 extends Application {
	private static final boolean USE_WIDTH_FIXED_SIZE = false;
	private static final boolean USE_HEIGHT_FIXED_SIZE = true;
//	private static final int COL_COUNT=30;
//	private static final int COL_COUNT=300;
//	private static final int COL_COUNT=600;
	private static final int COL_COUNT = 1000;
	private static final int ROW_COUNT = 1000;

	@Override
	public void start(final Stage primaryStage) throws Exception {
		final TableView<String[]> tableView = new TableView<>();

//	    tableView.setTableMenuButtonVisible(true); //too heavy
		if (USE_HEIGHT_FIXED_SIZE) {
			tableView.setFixedCellSize(24);
		}

		final ObservableList<TableColumn<String[], ?>> columns = tableView.getColumns();
		for (int i = 0; i < COL_COUNT; i++) {
			final TableColumn<String[], String> column = new TableColumn<>("Col" + i);
			final int colIndex = i;
			column.setCellValueFactory((cell) -> new SimpleStringProperty(cell.getValue()[colIndex]));
			columns.add(column);
			if (USE_WIDTH_FIXED_SIZE) {
				column.setPrefWidth(60);
				column.setMaxWidth(60);
				column.setMinWidth(60);
			}
		}

		final Button load = new Button("load");
		load.setOnAction(e -> {
			final ObservableList<String[]> items = tableView.getItems();
			items.clear();
			for (int i = 0; i < ROW_COUNT; i++) {
				final String[] rec = new String[COL_COUNT];
				for (int j = 0; j < rec.length; j++) {
					rec[j] = i + ":" + j;
				}
				items.add(rec);
			}
		});

		final Button reverse = new Button("reverse columns");
		reverse.setOnAction(e -> {
			final TableColumn<String[], ?>[] itemsArray = columns.toArray(new TableColumn[0]);
			Collections.reverse(Arrays.asList(itemsArray));
			tableView.getColumns().clear();
			tableView.getColumns().addAll(Arrays.asList(itemsArray));
		});

		final Button hide = new Button("hide % 10");
		hide.setOnAction(e -> {
			for (int i = 0, n = columns.size(); i < n; i++) {
				if (i % 10 == 0) {
					columns.get(i).setVisible(false);
				}
			}
		});

		final BorderPane root = new BorderPane(tableView);
		root.setTop(new HBox(8, load, reverse, hide));

		final Scene scene = new Scene(root, 800, 800);
		primaryStage.setScene(scene);
		primaryStage.show();
		this.prepareTimeline(scene);
	}

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

	private void prepareTimeline(final Scene scene) {
		new AnimationTimer() {
			@Override
			public void handle(final long now) {
				final double fps = com.sun.javafx.perf.PerformanceTracker.getSceneTracker(scene).getInstantFPS();
				((Stage) scene.getWindow()).setTitle("FPS:" + (int) fps);
			}
		}.start();
	}
}
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableColumn.CellDataFeatures;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;

public class BigTreeTableViewTest extends Application {

	private static final boolean USE_HEIGHT_FIXED_SIZE = true;
//	private static final int COL_COUNT = 900;
	private static final int COL_COUNT = 800;
//	private static final int COL_COUNT = 600;
//	private static final int COL_COUNT = 500;
//	private static final int COL_COUNT = 400;
//	private static final int COL_COUNT = 300;
//	private static final int COL_COUNT = 200;
//	private static final int COL_COUNT = 100;

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

	@Override
	public void start(final Stage stage) {
		final TreeItem<String> root = new TreeItem<>("Root");
		final TreeTableView<String> treeTableView = new TreeTableView<>(root);
		if (USE_HEIGHT_FIXED_SIZE) {
			treeTableView.setFixedCellSize(24);
		}
		treeTableView.setPrefWidth(800);
		treeTableView.setPrefHeight(500);
		stage.setWidth(800);
		stage.setHeight(500);

		Platform.runLater(() -> {
			for (int i = 0; i < 100; i++) {
				TreeItem<String> child = this.addNodes(root);
				child = this.addNodes(child);
				child = this.addNodes(child);
				child = this.addNodes(child);
			}
		});

		final TreeTableColumn<String, String>[] cols = new TreeTableColumn[COL_COUNT + 1];
		final TreeTableColumn<String, String> column = new TreeTableColumn<>("Column");
		column.setPrefWidth(150);
		column.setCellValueFactory(
				(final CellDataFeatures<String, String> p) -> new ReadOnlyStringWrapper(p.getValue().getValue()));
		cols[0] = column;

		for (int i = 0; i < COL_COUNT; i++) {
			final TreeTableColumn<String, String> col = new TreeTableColumn<>(Integer.toString(i));
			col.setPrefWidth(60);
			col.setCellValueFactory(val -> new ReadOnlyStringWrapper(val.getValue().getValue()+":"+val.getTreeTableColumn().getText()));
			cols[i + 1] = col;
		}
		treeTableView.getColumns().addAll(cols);

		final Scene scene = new Scene(treeTableView, 800, 500);
		stage.setScene(scene);
		stage.show();
		this.prepareTimeline(scene);
	}

	private TreeItem<String> addNodes(final TreeItem<String> parent) {

		final TreeItem<String>[] childNodes = new TreeItem[20];
		for (int i = 0; i < childNodes.length; i++) {
			childNodes[i] = new TreeItem<>("N" + i);
		}
		final TreeItem<String> root = new TreeItem<>("dir");
		root.setExpanded(true);
		root.getChildren().addAll(childNodes);
		parent.setExpanded(true);
		parent.getChildren().add(root);
		return root;
	}

	private void prepareTimeline(final Scene scene) {
		new AnimationTimer() {
			@Override
			public void handle(final long now) {
				final double fps = com.sun.javafx.perf.PerformanceTracker.getSceneTracker(scene).getInstantFPS();
				((Stage) scene.getWindow()).setTitle("FPS:" + (int) fps);
			}
		}.start();
	}

}

Progress

  • [x] Change must not contain extraneous whitespace
  • [x] Commit message must refer to an issue
  • [ ] Change must be properly reviewed

Issue

  • JDK-8185887: TableRowSkinBase fails to correctly virtualize cells in horizontal direction

Download

$ git fetch https://git.openjdk.java.net/jfx pull/125/head:pull/125 $ git checkout pull/125

yososs avatar Feb 24 '20 07:02 yososs

Hi yososs, welcome to this OpenJDK project and thanks for contributing!

We do not recognize you as Contributor and need to ensure you have signed the Oracle Contributor Agreement (OCA). If you have not signed the OCA, please follow the instructions. Please fill in your GitHub username in the "Username" field of the application. Once you have signed the OCA, please let us know by writing /signed in a comment in this pull request.

If you already are an OpenJDK Author, Committer or Reviewer, please click here to open a new issue so that we can record that fact. Please use "Add GitHub user yososs" as summary for the issue.

If you are contributing this work on behalf of your employer and your employer has signed the OCA, please let us know by writing /covered in a comment in this pull request.

bridgekeeper[bot] avatar Feb 24 '20 07:02 bridgekeeper[bot]

/signed

yososs avatar Feb 24 '20 07:02 yososs

Thank you! Please allow for up to two weeks to process your OCA, although it is usually done within one to two business days. Also, please note that pull requests that are pending an OCA check will not usually be evaluated, so your patience is appreciated!

bridgekeeper[bot] avatar Feb 24 '20 07:02 bridgekeeper[bot]

I'm signed to OCA and already a contributor.

yososs avatar Feb 24 '20 07:02 yososs

The title of your PR should match the title of the JBS issue. Moreover, #108 already tackles this issue with a different approach and 2 PRs on the same issue can't be intetgrated. Since I'm not familiar with this code, I can't advise on who's solution is more suitable. I suggest you discuss both of the approaches on the mailing list. I saw many JBS issues around this performance issue, probably one of them fits (or a new one can be created).

nlisker avatar Mar 09 '20 17:03 nlisker

Hi, I couldn't find you listed at https://www.oracle.com/technetwork/community/oca-486395.html . Please send me an e-mail at [email protected] with information about your OCA, so that I can look it up.

robilad avatar Apr 02 '20 17:04 robilad

My name is "Naohiro Yoshimoto" I have received an approved email on 2019-8-3.

yososs avatar Apr 03 '20 03:04 yososs

Webrevs

mlbridge[bot] avatar Apr 03 '20 10:04 mlbridge[bot]

/reviewers 2

kevinrushforth avatar Apr 14 '20 11:04 kevinrushforth

@kevinrushforth The number of required reviews for this PR is now set to 2 (with at least 1 of role reviewers).

openjdk[bot] avatar Apr 14 '20 11:04 openjdk[bot]

19fabf2eafcb02b519d39a1b0a9dad5b9209db64

  • Constructor creates multiple cell nodes, but the Fixed a wasteful problem of creating all cells and deleting out-of-display cells because the fixedCellSize attribute was not initialized in the constructor..
  • Reduce the load on ExpressionHelper.removeListener by removing cells outside of the display range from the back of the scrolling operation.

yososs avatar Apr 18 '20 17:04 yososs

When setting the cell height to a fixed value with setFixedCellSize(), column virtualization can improve performance.

As the next step, I think it is possible to try to enable column virtualization regardless of whether setFixedCellSize() is set or not. Before doing this step, the direct access validation of the internal structure of the test code needs to be changed via the public API.

yososs avatar Aug 27 '20 06:08 yososs

this indeed improves scrolling performance considerably (from extremely lagging to smooth following the mouse when dragging the thumb of the vertical scrollbar).

As a side-effect, start-up time of TreeTableView seems to increase considerably (at least by a factor of 3 to 4, probably not linear in # of columns) - see f.i. the example in JDK-8166956. Any idea why that might happen?

kleopatra avatar Aug 30 '20 13:08 kleopatra

Since there was a problem that the cell was not updated when the window was maximized, I added a fix to TreeTableViewSkin.java.

Added test code (BigTreeTableViewTest) for TreeTableView to the first post.

@kleopatra Does the test code I added (BigTreeTableViewTest.java) also have side effects?

yososs avatar Aug 31 '20 19:08 yososs

Since there was a problem that the cell was not updated when the window was maximized, I added a fix to TreeTableViewSkin.java.

that's just the added listener to hbar properties (same as in TableViewSkin), or anything else changed?

Added test code (BigTreeTableViewTest) for TreeTableView to the first post.

@kleopatra Does the test code I added (BigTreeTableViewTest.java) also have side effects?

yeah, but why do you ask me - you could easily see for yourself :) And don't understand why you wrap the hierarchy setup in runlater (which smears a bit over the delay by filling the treeTable content at an unspecific time "later")

kleopatra avatar Sep 01 '20 14:09 kleopatra

When the startup time is measured by eye, the impression changes depending on the individual difference. The effect of runLater affects your experience.

However, I succeeded in further improving performance by eliminating another bottleneck in TreeTableView. Of course, it also includes improvements in startup time.

I plan to commit at a later date.

yososs avatar Sep 01 '20 20:09 yososs

When the startup time is measured by eye, the impression changes depending on the individual difference.

my eye is a precision instrument :) Seriously, who would do such a thingy? Obviously, it must be measured, for zeroth approximation doing so crudely by comparing currentMillis before/after showing is good enough to "see" the tendency.

The effect of runLater affects your experience.

that's why you shouldn't do it when trying to gain insight into timing issues ;)

However, I succeeded in further improving performance by eliminating another bottleneck in TreeTableView. Of course, it also includes improvements in startup time.

cool :)

kleopatra avatar Sep 02 '20 10:09 kleopatra

Column virtualization causes shortness of breath when scrolling a large stroke horizontally. This does not happen when stroking on the scrollbar. Looks like a potential problem with VirtualFlow.

If you are worried about shortness of breath, the following code will help reduce the problem.

 private static final int OVERLAP_MARGIN = 500;

    private static boolean isOverlap(double start, double end, double start2, double end2){
    	start = Math.max(start-OVERLAP_MARGIN, start2);
    	end = Math.min(end+OVERLAP_MARGIN, end2);
        return (start<=end2 && end >= start2);
    }

yososs avatar Sep 03 '20 07:09 yososs

@yososs Per this message on the openjfx-dev mailing list, I have closed JDK-8185886 as a duplicate, and suggested another existing JBS issue for this PR to use. Please change the title to:

8185887: TableRowSkinBase fails to correctly virtualize cells in horizontal direction

kevinrushforth avatar Sep 08 '20 20:09 kevinrushforth

Below are the answers to JBS's suggestions.

The patch below resolves the issue, but it is not likely the correct solution (we are trying to determine the table width, but we are getting the width and padding on the underlying virtualflow (the width is fine, the padding should really come from tableview directly):

You probably mention this part, At least padding refers to Skinnable(TableRow or TreeTableRow). This is the same as before (L683). The width of VirtualFlow is determined by the header of Table.

modules/javafx.controls/src/main/java/javafx/scene/control/skin/TableRowSkinBase.java#L360-L365

 final VirtualFlow<?> virtualFlow = getVirtualFlow(); 
 final double scrollX = virtualFlow == null ? 0.0 : virtualFlow.getHbar().getValue(); 
 final Insets padding = getSkinnable().getPadding(); 
 final double vfWidth = virtualFlow == null ? 0.0:virtualFlow.getWidth(); 
 final double headerWidth = vfWidth - (padding.getLeft() + padding.getRight()); 

For example, can you assume a case that does not work properly?

yososs avatar Sep 09 '20 02:09 yososs

This is the answer to JBS's comment.

The BigTreeTableViewTest.java attached to this PR commentary is a modified version of the JDK-8166956 test code. The original test code is good for reproducing the problem, but I have decided that it cannot be used as a test code because the cell values are random and the cell values change randomly with the close / expand operation.

yososs avatar Sep 10 '20 05:09 yososs

@yososs Unknown command signed - for a list of valid commands use /help.

openjdk[bot] avatar Mar 17 '21 09:03 openjdk[bot]

Hi @sghpjuikit, thanks for making a comment in an OpenJDK project!

All comments and discussions in the OpenJDK Community must be made available under the OpenJDK Terms of Use. If you already are an OpenJDK Author, Committer or Reviewer, please click here to open a new issue so that we can record that fact. Please Use "Add GitHub user sghpjuikit for the summary.

If you are not an OpenJDK Author, Committer or Reviewer, simply check the box below to accept the OpenJDK Terms of Use for your comments.

Your comment will be automatically restored once you have accepted the OpenJDK Terms of Use.

sghpjuikit avatar Mar 20 '21 14:03 sghpjuikit

@yososs this pull request can not be integrated into master due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

git checkout 8185886_fix_scroll_performance_of_tableview
git fetch https://git.openjdk.org/jfx master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push

openjdk[bot] avatar Nov 14 '22 19:11 openjdk[bot]

@yososs Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. See OpenJDK Developers’ Guide for more information.

openjdk[bot] avatar Feb 17 '23 18:02 openjdk[bot]

@yososs This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

bridgekeeper[bot] avatar Mar 31 '23 23:03 bridgekeeper[bot]

@yososs This pull request has been inactive for more than 8 weeks and will now be automatically closed. If you would like to continue working on this pull request in the future, feel free to reopen it! This can be done using the /open pull request command.

bridgekeeper[bot] avatar Apr 29 '23 02:04 bridgekeeper[bot]