view analyzer/fx/src/main/java/jp/co/ntt/oss/heapstats/plugin/builtin/snapshot/SnapShotController.java @ 243:be9892e921b3

Bug 3420: Migrate Plugin API module from fx module Reviewed-by: yasuenag GitHub: https://github.com/HeapStats/heapstats/pull/109
author KUBOTA Yuji <kubota.yuji@lab.ntt.co.jp>
date Mon, 17 Jul 2017 23:44:04 +0900
parents 83d42891fa09
children
line wrap: on
line source

/*
 * Copyright (C) 2014-2017 Yasumasa Suenaga
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package jp.co.ntt.oss.heapstats.plugin.builtin.snapshot;

import java.io.File;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import jp.co.ntt.oss.heapstats.MainWindowController;
import jp.co.ntt.oss.heapstats.container.snapshot.ObjectData;
import jp.co.ntt.oss.heapstats.container.snapshot.SnapShotHeader;
import jp.co.ntt.oss.heapstats.container.snapshot.SummaryData;
import jp.co.ntt.oss.heapstats.plugin.PluginController;
import jp.co.ntt.oss.heapstats.plugin.builtin.snapshot.tabs.HistogramController;
import jp.co.ntt.oss.heapstats.plugin.builtin.snapshot.tabs.RefTreeController;
import jp.co.ntt.oss.heapstats.plugin.builtin.snapshot.tabs.SnapshotController;
import jp.co.ntt.oss.heapstats.plugin.builtin.snapshot.tabs.SummaryController;
import jp.co.ntt.oss.heapstats.task.CSVDumpGC;
import jp.co.ntt.oss.heapstats.task.CSVDumpHeap;
import jp.co.ntt.oss.heapstats.task.ParseHeader;
import jp.co.ntt.oss.heapstats.utils.HeapStatsUtils;
import jp.co.ntt.oss.heapstats.utils.TaskAdapter;

/**
 * FXML Controller of SnapShot builtin plugin.
 *
 * @author Yasumasa Suenaga
 */
public class SnapShotController extends PluginController implements Initializable {

    @FXML
    private SummaryController summaryController;

    @FXML
    private HistogramController histogramController;

    @FXML
    private SnapshotController snapshotController;

    @FXML
    private RefTreeController reftreeController;

    @FXML
    private SplitPane rangePane;

    @FXML
    private Label startTimeLabel;

    @FXML
    private Label endTimeLabel;

    @FXML
    private TextField snapshotList;

    @FXML
    private RadioButton radioInstance;

    @FXML
    private Button okBtn;

    @FXML
    private TabPane snapshotMain;

    @FXML
    private Tab histogramTab;

    @FXML
    private Tab snapshotTab;

    @FXML
    private Tab reftreeTab;

    private ObjectProperty<ObservableList<SnapShotHeader>> currentTarget;

    private ObjectProperty<SummaryData> summaryData;

    private ObjectProperty<ObservableSet<String>> currentClassNameSet;

    private ObjectProperty<SelectionModel<SnapShotHeader>> snapshotSelectionModel;

    private ObjectProperty<ObservableMap<LocalDateTime, List<ObjectData>>> topNList;

    private List<SnapShotHeader> snapShotHeaders;

    private ObjectProperty<SnapShotHeader> currentSnapShotHeader;

    private LongProperty currentObjectTag;

    private ObjectProperty<LocalDateTime> rangeStart;

    private ObjectProperty<LocalDateTime> rangeEnd;

    /**
     * Update caption of label which represents time of selection.
     *
     * @param target Label compornent to draw.
     * @param newValue Percentage of timeline. This value is between 0.0 and 1.0 .
     */
    private void updateRangeLabel(Label target, double newValue){
        if(!Optional.ofNullable(snapShotHeaders).map(List::isEmpty).orElse(true)){
            LocalDateTime start = snapShotHeaders.get(0).getSnapShotDate();
            LocalDateTime end = snapShotHeaders.get(snapShotHeaders.size() - 1).getSnapShotDate();
            long diff = start.until(end, ChronoUnit.MILLIS);
            LocalDateTime newTime = start.plus((long)(diff * (Math.round(newValue * 100.0d) / 100.0d)), ChronoUnit.MILLIS);

            if(target == startTimeLabel){
                rangeStart.set(newTime.truncatedTo(ChronoUnit.SECONDS));
            }
            else{
                rangeEnd.set(newTime.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS));
            }

            target.setText(newTime.format(HeapStatsUtils.getDateTimeFormatter()));
        }
    }

    /**
     * Initializes the controller class.
     */
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        super.initialize(url, rb);

        summaryData = new SimpleObjectProperty<>();
        summaryController.summaryDataProperty().bind(summaryData);
        currentTarget = new SimpleObjectProperty<>(FXCollections.emptyObservableList());
        summaryController.currentTargetProperty().bind(currentTarget);
        histogramController.currentTargetProperty().bind(currentTarget);
        snapshotController.currentTargetProperty().bind(currentTarget);
        currentClassNameSet = new SimpleObjectProperty<>();
        summaryController.currentClassNameSetProperty().bind(currentClassNameSet);
        histogramController.currentClassNameSetProperty().bind(currentClassNameSet);
        histogramController.instanceGraphProperty().bind(radioInstance.selectedProperty());
        snapshotController.instanceGraphProperty().bind(radioInstance.selectedProperty());
        snapshotSelectionModel = new SimpleObjectProperty<>();
        snapshotSelectionModel.bind(snapshotController.snapshotSelectionModelProperty());
        histogramController.snapshotSelectionModelProperty().bind(snapshotSelectionModel);
        histogramController.setDrawRebootSuspectLine(this::drawRebootSuspectLine);
        topNList = new SimpleObjectProperty<>();
        topNList.bind(histogramController.topNListProperty());
        snapshotController.topNListProperty().bind(topNList);
        currentSnapShotHeader = new SimpleObjectProperty<>();
        currentSnapShotHeader.bind(snapshotController.snapshotSelectionModelProperty().get().selectedItemProperty());
        reftreeController.currentSnapShotHeaderProperty().bind(currentSnapShotHeader);

        currentObjectTag = new SimpleLongProperty();
        // TODO:
        //   Why can I NOT use binding?
        //   First binding is enabled. But second binding is disabled.
        //   So I use ChangeListener to avoid this issue.
        //currentObjectTag.bind(histogramController.currentObjectTagProperty());
        //currentObjectTag.bind(snapshotController.currentObjectTagProperty());
        histogramController.currentObjectTagProperty().addListener((v, o, n) -> Optional.ofNullable(n).ifPresent(m -> currentObjectTag.set((Long) m)));
        snapshotController.currentObjectTagProperty().addListener((v, o, n) -> Optional.ofNullable(n).ifPresent(m -> currentObjectTag.set((Long) m)));
        reftreeController.currentObjectTagProperty().bind(currentObjectTag);

        snapshotMain.getSelectionModel().selectedItemProperty().addListener(this::onTabChanged);

        snapShotHeaders = null;
        rangeStart = new SimpleObjectProperty<>();
        rangeEnd = new SimpleObjectProperty<>();

        rangePane.getDividers().get(0).positionProperty().addListener((v, o, n) -> updateRangeLabel(startTimeLabel, n.doubleValue()));
        rangePane.getDividers().get(1).positionProperty().addListener((v, o, n) -> updateRangeLabel(endTimeLabel, n.doubleValue()));

        setOnWindowResize((v, o, n) -> Platform.runLater(() -> Stream.of(summaryController.getHeapChart(),
                summaryController.getInstanceChart(),
                summaryController.getGcTimeChart(),
                summaryController.getMetaspaceChart(),
                histogramController.getTopNChart())
                .forEach(c -> Platform.runLater(() -> drawRebootSuspectLine(c)))));

        histogramController.setTaskExecutor(t -> {
            bindTask(t);
            (new Thread(t)).start();
        });
    }

    private void onTabChanged(ObservableValue<? extends Tab> observable, Tab oldValue, Tab newValue) {
        if (newValue == reftreeTab) {
            if (oldValue == histogramTab) {
                currentObjectTag.set(histogramController.currentObjectTagProperty().get());
            } else if (oldValue == snapshotTab) {
                currentObjectTag.set(snapshotController.currentObjectTagProperty().get());
            }
        }
    }

    /**
     * onSucceeded event handler for ParseHeader.
     *
     * @param headers New SnapShotHeader list.
     */
    private void onSnapShotParserSucceeded(List<SnapShotHeader> headers) {
        snapShotHeaders = headers;

        rangePane.getDividers().get(0).setPosition(0.0d);
        rangePane.getDividers().get(1).setPosition(1.0d);

        rangePane.setDisable(false);
        okBtn.setDisable(false);
    }

    /**
     * Event handler of SnapShot file button.
     *
     * @param event ActionEvent of this event.
     */
    @FXML
    public void onSnapshotFileClick(ActionEvent event) {
        FileChooser dialog = new FileChooser();
        ResourceBundle resource = ResourceBundle.getBundle("snapshotResources", new Locale(HeapStatsUtils.getLanguage()));

        dialog.setTitle(resource.getString("dialog.filechooser.title"));
        dialog.setInitialDirectory(new File(HeapStatsUtils.getDefaultDirectory()));
        dialog.getExtensionFilters().addAll(new ExtensionFilter("SnapShot file (*.dat)", "*.dat"),
                new ExtensionFilter("All files", "*.*"));

        List<File> snapshotFileList = dialog.showOpenMultipleDialog(MainWindowController.getInstance().getOwner());

        if (snapshotFileList != null) {
            clearAllItems();
            
            HeapStatsUtils.setDefaultDirectory(snapshotFileList.get(0).getParent());
            List<String> files = snapshotFileList.stream()
                    .map(File::getAbsolutePath)
                    .collect(Collectors.toList());
            snapshotList.setText(files.stream().collect(Collectors.joining("; ")));

            TaskAdapter<ParseHeader> task = new TaskAdapter<>(new ParseHeader(files, HeapStatsUtils.getReplaceClassName(), true));
            task.setOnSucceeded(evt -> onSnapShotParserSucceeded(task.getTask().getSnapShotList()));
            super.bindTask(task);

            Thread parseThread = new Thread(task);
            parseThread.start();
        }

    }

    /**
     * Event handler of OK button.
     *
     * @param event ActionEvent of this event.
     */
    @FXML
    private void onOkClick(ActionEvent event) {
        /* Get range */
        LocalDateTime start = rangeStart.getValue();
        LocalDateTime end = rangeEnd.getValue();
        currentTarget.set(FXCollections.observableArrayList(snapShotHeaders.stream()
                                                                           .filter(d -> ((d.getSnapShotDate().compareTo(start) >= 0) && (d.getSnapShotDate().compareTo(end) <= 0)))
                                                                           .collect(Collectors.toList())));
        currentClassNameSet.set(FXCollections.observableSet());
        summaryData.set(new SummaryData(currentTarget.get()));

        Task<Void> topNTask = histogramController.getDrawTopNDataTask(currentTarget.get(), true, null);
        super.bindTask(topNTask);
        Thread topNThread = new Thread(topNTask);
        topNThread.start();

        Task<Void> summarizeTask = summaryController.getCalculateGCSummaryTask(this::drawRebootSuspectLine);
        super.bindTask(summarizeTask);
        Thread summarizeThread = new Thread(summarizeTask);
        summarizeThread.start();
    }

    /**
     * Returns plugin name. This value is used to show in main window tab.
     *
     * @return Plugin name.
     */
    @Override
    public String getPluginName() {
        return "SnapShot Data";
    }

    @Override
    public EventHandler<Event> getOnPluginTabSelected() {
        return null;
    }

    @Override
    public String getLicense() {
        return PluginController.LICENSE_GPL_V2;
    }

    @Override
    public Map<String, String> getLibraryLicense() {
        Map<String, String> licenseMap = new HashMap<>();
        licenseMap.put("JGraphX", PluginController.LICENSE_BSD);

        return licenseMap;
    }

    /**
     * Dump GC Statistics to CSV.
     *
     * @param isSelected If this value is true, this method dumps data which is
     * selected time range, otherwise this method dumps all snapshot data.
     */
    public void dumpGCStatisticsToCSV(boolean isSelected) {
        FileChooser dialog = new FileChooser();
        dialog.setTitle("Select CSV files");
        dialog.setInitialDirectory(new File(HeapStatsUtils.getDefaultDirectory()));
        dialog.getExtensionFilters().addAll(new ExtensionFilter("CSV file (*.csv)", "*.csv"),
                new ExtensionFilter("All files", "*.*"));
        File csvFile = dialog.showSaveDialog(MainWindowController.getInstance().getOwner());

        if (csvFile != null) {
            TaskAdapter<CSVDumpGC> task = new TaskAdapter<>(new CSVDumpGC(csvFile, isSelected ? currentTarget.get() : snapShotHeaders));
            super.bindTask(task);

            Thread parseThread = new Thread(task);
            parseThread.start();
        }

    }

    /**
     * Dump Java Class Histogram to CSV.
     *
     * @param isSelected If this value is true, this method dumps data which is
     * selected in class filter, otherwise this method dumps all snapshot data.
     */
    public void dumpClassHistogramToCSV(boolean isSelected) {
        FileChooser dialog = new FileChooser();
        ResourceBundle resource = ResourceBundle.getBundle("snapshotResources", new Locale(HeapStatsUtils.getLanguage()));

        dialog.setTitle(resource.getString("dialog.csvchooser.title"));
        dialog.setInitialDirectory(new File(HeapStatsUtils.getDefaultDirectory()));
        dialog.getExtensionFilters().addAll(new ExtensionFilter("CSV file (*.csv)", "*.csv"),
                new ExtensionFilter("All files", "*.*"));
        File csvFile = dialog.showSaveDialog(MainWindowController.getInstance().getOwner());

        if (csvFile != null) {
            Predicate<? super ObjectData> filter = histogramController.getFilter();
            TaskAdapter<CSVDumpHeap> task = new TaskAdapter<>(new CSVDumpHeap(csvFile, isSelected ? currentTarget.get() : snapShotHeaders, isSelected ? filter : null, HeapStatsUtils.getReplaceClassName()));
            super.bindTask(task);

            Thread parseThread = new Thread(task);
            parseThread.start();
        }

    }

    @Override
    public Runnable getOnCloseRequest() {
        return null;
    }

    @Override
    public void setData(Object data, boolean select) {
        super.setData(data, select);
        snapshotList.setText((String) data);

        TaskAdapter<ParseHeader> task = new TaskAdapter<>(new ParseHeader(Arrays.asList((String) data), HeapStatsUtils.getReplaceClassName(), true));
        task.setOnSucceeded(evt -> onSnapShotParserSucceeded(task.getTask().getSnapShotList()));
        super.bindTask(task);

        Thread parseThread = new Thread(task);
        parseThread.start();
    }

    private void drawRebootSuspectLine(XYChart<Number, ? extends Number> target) {

        if (target.getData().isEmpty() || target.getData().get(0).getData().isEmpty()) {
            return;
        }

        AnchorPane anchor = (AnchorPane) target.getParent().getChildrenUnmodifiable()
                .stream()
                .filter(n -> n instanceof AnchorPane)
                .findAny()
                .get();
        ObservableList<Node> anchorChildren = anchor.getChildren();
        anchorChildren.clear();

        NumberAxis xAxis = (NumberAxis)target.getXAxis();
        Axis yAxis = target.getYAxis();
        Label chartTitle = (Label) target.getChildrenUnmodifiable().stream()
                .filter(n -> n.getStyleClass().contains("chart-title"))
                .findFirst()
                .get();

        double startX = xAxis.getLayoutX() + 4.0d;
        double yPos = yAxis.getLayoutY() + chartTitle.getLayoutY() + chartTitle.getHeight();
        List<Rectangle> rectList = summaryData.get().getRebootSuspectList()
                .stream()
                .map(d -> d.atZone(ZoneId.systemDefault()).toEpochSecond())
                .map(s -> new Rectangle(xAxis.getDisplayPosition(s) + startX, yPos, 4d, yAxis.getHeight()))
                .peek(r -> ((Rectangle) r).setStyle("-fx-fill: yellow;"))
                .collect(Collectors.toList());
        anchorChildren.addAll(rectList);
    }

    /**
     * Clear all items in SnapShot plugin.
     */
    public void clearAllItems(){
        currentTarget.set(FXCollections.emptyObservableList());
        summaryData.set(null);
        currentClassNameSet.set(FXCollections.emptyObservableSet());
        currentObjectTag.set(-1);
        
        summaryController.clearAllItems();
        histogramController.clearAllItems();
        snapshotController.clearAllItems();
}
    
}