JavaFX の TreeView の展開のパフォーマンス問題のワークアラウンド

この記事では TreeViewのパフォーマンス問題 [JDK-8200099] の回避方法を紹介します。 ここで紹介する方法を使うことによって、実用的レスポンスでTreeViewの扱えるデータサイズが10 倍に増えます。

ここで紹介するコードは一時的な解決です。将来の JavaFX のバージョンでは動作しないかもしれないので注意してください。

問題

TreeView will take an exponential time for the number of additional items in the root grandchild item.

この問題は私が2018年に報告していたもので、私の見つけたワークアラウンドの紹介もしています。 TreeView の子ノードの数を10倍ずつ増やして展開のレスポンスを実際に動作させて確認していくと、 10万件でフリーズしたように見えます。TreeView を実用的なレスポンスで使えるのは子ノード数が1万件まであることが分かります。

1万件から10万件までのレスポンスタイムを1万ステップで実際に測定して、 傾向を見ると指数関数的なレスポンスタイムの増加が見られます。これは線形になるべきです。

100件程度では体感できないとしてもバッテリー駆動環境では徹底的に排除されるべき無駄が隠れていると推測できます。

Node-0 の展開レスポンス
子ノード数レスポンスタイム(msec)

10000

410

20000

2638

30000

8095

40000

8095

50000

21936

60000

30208

70000

40855

80000

68735

90000

93978

100000

105267

測定ではJVM の起動パラメタに -Xms512m -Xmx512m を使用しています。

環境

  • JavaFX 8, 9, 10, 11

解決

レスポンスの測定結果から TreeView のノードの展開処理には性能上の深刻な問題があることが分かります。

私はプロファイルと目視のコードレビューでこの問題の実装箇所を特定しています。

次のテストコードでUSE_WORKAROUND=true を設定するとパフォーマンスが改善されていることを確認できます。

JDK-8200099 のテストコード
public final class TreeViewPerformanceTest extends Application{


private static boolean USE_WORKAROUND = false;
private static final int TREE_DATA_SIZE = 1000*100;

private StringProperty reportProperty;
private TreeView<String> treeView;

@Override
public void start(Stage primaryStage) throws Exception {
final TextArea report = new TextArea();
this.reportProperty = report.textProperty();
buildTreeView();
BorderPane borderPane = new BorderPane();
borderPane.setCenter(new StackPane(treeView));
borderPane.setBottom(report);

final Scene scene = new Scene(borderPane, 800, 800);
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}

private void buildTreeView() {
this.treeView = new TreeView<String>();
treeView.setFixedCellSize(25);
final LazyLoadTreeItem treeItem = new LazyLoadTreeItem("Node");
treeItem.setExpanded(true);
treeView.setRoot(treeItem);
treeView.setEditable(false);

if(USE_WORKAROUND) {
treeView.expandedItemCountProperty().addListener((ob,oldValue,newValue)->{
int scrollToIndex = treeView.getSelectionModel().getSelectedIndex();
treeView.scrollTo(scrollToIndex);
});
}
}

private class LazyLoadTreeItem extends TreeItem<String>{
LazyLoadTreeItem(String title){
super(title);
}
@Override
public ObservableList getChildren() {
if(this.loaded) {
return super.getChildren();
}
long t = System.currentTimeMillis();
this.loaded = true;
final String value = getValue();

final LazyLoadTreeItem[] d1a = new LazyLoadTreeItem[TREE_DATA_SIZE];
for(int i=0; i<d1a.length; i++) {
d1a[i] = new LazyLoadTreeItem(value + '-' + String.valueOf(i));
}
final ObservableList<TreeItem<String>> children = super.getChildren();
TreeItem<String> selectedTreeItem = treeView.getSelectionModel().getSelectedItem();

if(USE_WORKAROUND) {
try {
treeView.getSelectionModel().clearSelection();
children.setAll(d1a);
treeView.getSelectionModel().select(selectedTreeItem);
int scrollToIndex = treeView.getRow(selectedTreeItem);
treeView.scrollTo(scrollToIndex);
treeView.refresh();
// Platform.runLater(()->{
// int scrollToIndex = treeView.getRow(selectedTreeItem);
// treeView.scrollTo(scrollToIndex);
// });

}catch (Exception e) {
e.printStackTrace();
}
}else {
children.setAll(d1a);
}

String result = value + ": time: " + (System.currentTimeMillis() - t);
reportProperty.set( reportProperty.get() + "\n" + result );
return children;
}

@Override
public boolean isLeaf() {
return false;
}

boolean loaded;
}

public static void main(String[] args) {
Application.launch(args);
}
}
ワークアラウンド適用後のNode-0 の展開レスポンス
子ノード数レスポンスタイム(msec)

10000

39

20000

77

30000

118

40000

126

50000

246

60000

227

70000

237

80000

263

90000

225

100000

156

ワークアラウンド適用後のレスポンスタイムを確認すると10万件までが実用的な範囲の子ノード数であることが分かります。 この測定結果から10倍の性能改善ができたと言えるでしょう。 (10万件までの測定では件数以外の他の要因の影響が大きくなっています。)

しかしながら、TreeView の展開処理は1件の追加処理をN回繰り返すような冗長な記述になっており、 ここで紹介するワークアラウンドでは問題のあるコードを完全には回避していません。 TreeView の内部の多重ループの実装を直接修正することで、 あと一桁くらいレスポンスタイムを改善できる余地があるように思います。

YOSBITS では JavaFX のコンサルティングを行なっていますので、 他に質問があればメールで承ります。 コンタクトを参照してください。