/** * erlyberly, erlang trace debugger * Copyright (C) 2016 Andy Till * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package erlyberly; import java.text.DecimalFormat; import java.net.URL; import java.util.ResourceBundle; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangString; import de.jensd.fx.fontawesome.AwesomeIcon; import floatyfield.FloatyFieldView; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.binding.BooleanBinding; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Orientation; import javafx.scene.chart.PieChart; import javafx.scene.chart.PieChart.Data; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Control; import javafx.scene.control.MenuItem; import javafx.scene.control.SelectionMode; import javafx.scene.control.Separator; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Callback; import ui.FAIcon; /** * Handles UI related tasks and delegates processing to {@link ProcController}. */ public class ProcView implements Initializable { private final ProcController procController; @FXML private TableView<ProcInfo> processView; @FXML private Button refreshButton; @FXML private Button pollButton; @FXML private Button heapPieButton; @FXML private Button stackPieButton; @FXML private Button totalHeapPieButton; @FXML private HBox headerBox; /** * Total of the last opened pie chart */ private double total = 0d; /** * Only use on the javafx thread */ private final DecimalFormat percentFormatter = new DecimalFormat("#.#"); public ProcView() { procController = new ProcController(); } @Override @SuppressWarnings("unchecked") public void initialize(URL url, ResourceBundle r) { MenuItem menuItem; menuItem = new MenuItem("Get process state (R16B03 or higher)"); menuItem.setOnAction(this::onShowProcessStateClicked); menuItem.disableProperty().bind(processView.getSelectionModel().selectedItemProperty().isNull()); processView.setContextMenu(new ContextMenu(menuItem)); // #23 when the context menu is showing, temporarily suspend polling, polling // loses selection making the right click context menu no longer enabled since // no process is selected processView .getContextMenu() .showingProperty() .addListener((o, oldv, newv) -> { procController.setTemporarilySuspendPolling(newv); }); final BooleanBinding notConnected = ErlyBerly.nodeAPI().connectedProperty().not(); ErlyBerly.nodeAPI().connectedProperty().addListener(this::onConnected); heapPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART)); heapPieButton.getStyleClass().add("erlyberly-icon-button"); heapPieButton.setStyle("-fx-background-color: transparent;"); heapPieButton.setText(""); heapPieButton.disableProperty().bind(notConnected); stackPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART)); stackPieButton.setStyle("-fx-background-color: transparent;"); stackPieButton.setText(""); stackPieButton.disableProperty().bind(notConnected); totalHeapPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART)); totalHeapPieButton.setStyle("-fx-background-color: transparent;"); totalHeapPieButton.setText(""); totalHeapPieButton.disableProperty().bind(notConnected); refreshButton.setGraphic(FAIcon.create().icon(AwesomeIcon.ROTATE_LEFT)); refreshButton.setGraphicTextGap(8d); refreshButton.disableProperty().bind(procController.pollingProperty().or(notConnected)); pollButton.setGraphic(FAIcon.create().icon(AwesomeIcon.REFRESH)); pollButton.setGraphicTextGap(9d); pollButton.disableProperty().bind(notConnected); procController.pollingProperty().addListener(this::onPollingChange); procController.setListComparator(processView.comparatorProperty()); onPollingChange(null); TableColumn<ProcInfo, String> pidColumn = (TableColumn<ProcInfo, String>) processView.getColumns().get(0); TableColumn<ProcInfo, String> procColumn = (TableColumn<ProcInfo, String>) processView.getColumns().get(1); TableColumn<ProcInfo, Long> reducColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(2); TableColumn<ProcInfo, Long> mQueueLenColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(3); TableColumn<ProcInfo, Long> heapSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(4); TableColumn<ProcInfo, Long> stackSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(5); TableColumn<ProcInfo, Long> totalHeapSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(6); pidColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, String>("pid")); pidColumn.setId("pid"); procColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, String>("processName")); procColumn.setId("proc"); reducColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("reductions")); reducColumn.setId("reduc"); mQueueLenColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("msgQueueLen")); mQueueLenColumn.setId("mqueue"); heapSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("heapSize")); heapSizeColumn.setId("heapsize"); stackSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("stackSize")); stackSizeColumn.setId("stacksize"); totalHeapSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("totalHeapSize")); totalHeapSizeColumn.setId("totalheapsize"); processView.setItems(procController.getProcs()); processView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); addFloatySearchControl(); initialiseProcessSorting(); } private FxmlLoadable addFloatySearchControl() { FxmlLoadable loader = new FxmlLoadable("/floatyfield/floaty-field.fxml"); loader.load(); FloatyFieldView ffView; ffView = (FloatyFieldView) loader.controller; ffView.promptTextProperty().set("Filter on process pid and registered name"); HBox.setHgrow(loader.fxmlNode, Priority.ALWAYS); headerBox.getChildren().add(0, new Separator(Orientation.VERTICAL)); headerBox.getChildren().add(0, loader.fxmlNode); procController.filterProperty().bind(ffView.textProperty()); Platform.runLater(() -> { FilterFocusManager.addFilter((Control) loader.fxmlNode.getChildrenUnmodifiable().get(1), 0); }); return loader; } private void onShowProcessStateClicked(ActionEvent e) { ProcInfo proc = processView.getSelectionModel().getSelectedItem(); if(proc == null) return; procController.processState(proc, (eobj) -> {showProcessStateInWindow(proc, eobj); }); } private void showProcessStateInWindow(ProcInfo procInfo, OtpErlangObject obj) { if(obj == null) obj = new OtpErlangString("Error, erlyberly cannot get process state. Probably not OTP compliant process"); TermTreeView termTreeView; termTreeView = new TermTreeView(); termTreeView.setMaxHeight(Integer.MAX_VALUE); VBox.setVgrow(termTreeView, Priority.ALWAYS); termTreeView.populateFromTerm(obj); ErlyBerly.showPane("Process State for " + procInfo.getShortName(), ErlyBerly.wrapInPane(termTreeView)); } @FXML private void onHeapPie() { ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getHeapSize(); }); showPieChart("Process Heap", data); } @FXML private void onStackPie() { ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getStackSize(); }); showPieChart("Process Stack", data); } @FXML private void onTotalHeapPie() { ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getTotalHeapSize(); }); showPieChart("Total Heap", data); } private ObservableList<PieChart.Data> buildData(ObservableList<ProcInfo> procs, Callback<ProcInfo, Long> extractor) { total = 0; for (ProcInfo proc : procs) { total += extractor.call(proc); } // threshold is 0.5%, this is a limit on how many segments are added to // the pie chart too many seems to crash the process double threshold = total / 200; double other = 0; ObservableList<PieChart.Data> data = FXCollections.observableArrayList(); for (ProcInfo proc : procs) { double value = extractor.call(proc); if(value >= threshold) data.add(new Data(procDescription(proc), extractor.call(proc))); else other += value; } if(other > 0) data.add(new Data("All processes less than 0.5% of total", other)); return data; } private ObservableList<ProcInfo> chartableProcs() { ObservableList<ProcInfo> procs = processView.getSelectionModel().getSelectedItems(); if(procs.isEmpty() || procs.size() == 1) { procs = procController.getProcs(); } return procs; } private void showPieChart(String title, ObservableList<PieChart.Data> data) { PieChart pieChart; pieChart = new PieChart(data); pieChart.setTitle(title); pieChart.getData().stream().forEach(d -> { Tooltip tooltip; tooltip = new Tooltip(); String percent = percentFormatter.format((d.getPieValue()/total)*100); tooltip.setText(d.getName() + " " + percent + "%"); Tooltip.install(d.getNode(), tooltip); }); ErlyBerly.showPane(title, ErlyBerly.wrapInPane(pieChart)); } private String procDescription(ProcInfo proc) { String pid = proc.getProcessName(); if(pid == null || "".equals(pid)) { pid = proc.getPid(); } if(pid == null || "".equals(pid)) { pid = "unknown pid"; } return pid; } private void onPollingChange(Observable o) { if(procController.pollingProperty().get()) pollButton.setText("Stop Polling"); else pollButton.setText("Start Polling"); } @FXML private void onRefresh() { procController.refreshOnce(); } @FXML private void onTogglePolling() { procController.togglePolling(); } private void onConnected(Observable o) { boolean connected = ErlyBerly.nodeAPI().connectedProperty().get(); if(connected) { procController.refreshOnce(); } else { procController.clearProcesses(); } } private void initialiseProcessSorting() { InvalidationListener invalidationListener = new ProcSortUpdater(); for (TableColumn<ProcInfo, ?> col : processView.getColumns()) { col.sortTypeProperty().addListener(invalidationListener); } processView.getSortOrder().addListener(invalidationListener); } private final class ProcSortUpdater implements InvalidationListener { @Override public void invalidated(Observable ob) { ProcSort procSort = null; if(!processView.getSortOrder().isEmpty()) { TableColumn<ProcInfo, ?> tableColumn = processView.getSortOrder().get(0); procSort = new ProcSort(tableColumn.getId(), tableColumn.getSortType()); } procController.procSortProperty().set(procSort); } } }