view vm-profiler/client-swing/src/main/java/com/redhat/thermostat/vm/profiler/client/swing/internal/SwingVmProfileView.java @ 1832:ffd37dfffbdd

Redesign ActionToggleButton states. Use in Profiling, Threads, Notifications Reviewed-by: neugens, jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-September/016403.html http://icedtea.classpath.org/pipermail/thermostat/2015-October/016608.html PR2671
author Andrew Azores <aazores@redhat.com>
date Tue, 13 Oct 2015 12:48:05 -0400
parents 4281c1d18ea0
children
line wrap: on
line source

/*
 * Copyright 2012-2015 Red Hat, Inc.
 *
 * This file is part of Thermostat.
 *
 * Thermostat 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, or (at your
 * option) any later version.
 *
 * Thermostat 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 Thermostat; see the file COPYING.  If not see
 * <http://www.gnu.org/licenses/>.
 *
 * Linking this code with other modules is making a combined work
 * based on this code.  Thus, the terms and conditions of the GNU
 * General Public License cover the whole combination.
 *
 * As a special exception, the copyright holders of this code give
 * you permission to link this code with independent modules to
 * produce an executable, regardless of the license terms of these
 * independent modules, and to copy and distribute the resulting
 * executable under terms of your choice, provided that you also
 * meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module
 * which is not derived from or based on this code.  If you modify
 * this code, you may extend this exception to your version of the
 * library, but you are not obligated to do so.  If you do not wish
 * to do so, delete this exception statement from your version.
 */

package com.redhat.thermostat.vm.profiler.client.swing.internal;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JToggleButton;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;

import com.redhat.thermostat.client.swing.EdtHelper;
import com.redhat.thermostat.client.swing.IconResource;
import com.redhat.thermostat.client.swing.NonEditableTableModel;
import com.redhat.thermostat.client.swing.SwingComponent;
import com.redhat.thermostat.client.swing.components.ActionToggleButton;
import com.redhat.thermostat.client.swing.components.FontAwesomeIcon;
import com.redhat.thermostat.client.swing.components.HeaderPanel;
import com.redhat.thermostat.client.swing.components.Icon;
import com.redhat.thermostat.client.swing.components.ThermostatScrollPane;
import com.redhat.thermostat.client.swing.components.ThermostatTable;
import com.redhat.thermostat.client.swing.experimental.ComponentVisibilityNotifier;
import com.redhat.thermostat.client.ui.Palette;
import com.redhat.thermostat.common.ActionEvent;
import com.redhat.thermostat.common.ActionListener;
import com.redhat.thermostat.common.utils.StringUtils;
import com.redhat.thermostat.common.utils.MethodDescriptorConverter.MethodDeclaration;
import com.redhat.thermostat.shared.locale.Translate;
import com.redhat.thermostat.vm.profiler.client.core.ProfilingResult;
import com.redhat.thermostat.vm.profiler.client.core.ProfilingResult.MethodInfo;

public class SwingVmProfileView extends VmProfileView implements SwingComponent {

    private static final Icon START_ICON = IconResource.SAMPLE.getIcon();
    private static final Icon STOP_ICON = new FontAwesomeIcon('\uf04d', START_ICON.getIconHeight());

    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();

    private static final double SPLIT_PANE_RATIO = 0.3;

    private final CopyOnWriteArrayList<ActionListener<ProfileAction>> listeners = new CopyOnWriteArrayList<>();

    private HeaderPanel mainContainer;

    private ActionToggleButton toggleButton;

    private DefaultListModel<Profile> listModel;
    private JList<Profile> profileList;

    private DefaultTableModel tableModel;

    private JLabel currentStatusLabel;
    private boolean viewControlsEnabled = true;

    static class ProfileItemRenderer extends DefaultListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList<?> list,
                Object value, int index, boolean isSelected,
                boolean cellHasFocus) {
            if (value instanceof Profile) {
                Profile profile = (Profile) value;
                value = translator
                        .localize(LocaleResources.PROFILER_LIST_ITEM,
                                profile.name, new Date(profile.timeStamp).toString())
                        .getContents();
            }
            return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        }
    }

    public SwingVmProfileView() {
        listModel = new DefaultListModel<>();

        toggleButton = new ActionToggleButton(START_ICON, STOP_ICON, translator.localize(LocaleResources.START_PROFILING));
        toggleButton.toggleText(false);
        toggleButton.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent e) {
                JToggleButton button = (JToggleButton) e.getSource();
                if (button.isSelected()) {
                    fireProfileAction(ProfileAction.START_PROFILING);
                } else {
                    fireProfileAction(ProfileAction.STOP_PROFILING);
                }
            }
        });

        mainContainer = new HeaderPanel(translator.localize(LocaleResources.PROFILER_HEADING));
        new ComponentVisibilityNotifier().initialize(mainContainer, notifier);

        JPanel contentContainer = new JPanel(new BorderLayout());
        mainContainer.setContent(contentContainer);
        mainContainer.addToolBarButton(toggleButton);

        JComponent actionsPanel = createStatusPanel();
        contentContainer.add(actionsPanel, BorderLayout.PAGE_START);

        JComponent profilingResultsPanel = createInformationPanel();
        contentContainer.add(profilingResultsPanel, BorderLayout.CENTER);
    }

    private JPanel createStatusPanel() {
        GridBagLayout layout = new GridBagLayout();
        JPanel statusPanel = new JPanel(layout);

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.anchor = GridBagConstraints.PAGE_START;
        constraints.weightx = 1.0;
        constraints.gridy = 0;
        constraints.gridx = 0;
        constraints.gridwidth = 3;
        constraints.ipady = 5;

        String wrappedText = "<html>" + translator.localize(LocaleResources.PROFILER_DESCRIPTION).getContents() + "</html>";
        JLabel descriptionLabel = new JLabel(wrappedText);
        statusPanel.add(descriptionLabel, constraints);

        constraints.gridy = 1;
        constraints.gridx = 0;
        constraints.gridwidth = 1;
        currentStatusLabel = new JLabel("Current Status: {0}");
        statusPanel.add(currentStatusLabel, constraints);
        return statusPanel;
    }

    private JComponent createInformationPanel() {
        if (!SwingUtilities.isEventDispatchThread()) {
            throw new AssertionError("Not in the EDT!");
        }

        profileList = new JList<>(listModel);
        profileList.setCellRenderer(new ProfileItemRenderer());
        profileList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        profileList.addListSelectionListener(new ListSelectionListener() {

            private Profile oldValue = null;

            @Override
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting()) {
                    return;
                }

                Profile newValue = profileList.getSelectedValue();

                if (oldValue == null || !oldValue.equals(newValue)) {
                    oldValue = newValue;
                    fireProfileAction(ProfileAction.PROFILE_SELECTED);
                }

            }
        });
        ThermostatScrollPane profileListPane = new ThermostatScrollPane(profileList);

        Vector<String> columnNames = new Vector<>();
        columnNames.add(translator.localize(LocaleResources.PROFILER_RESULTS_METHOD).getContents());
        columnNames.add(translator.localize(LocaleResources.PROFILER_RESULTS_PERCENTAGE_TIME).getContents());
        columnNames.add(translator.localize(LocaleResources.PROFILER_RESULTS_TIME, "ms").getContents());
        tableModel = new NonEditableTableModel(columnNames, 0) {
            @Override
            public java.lang.Class<?> getColumnClass(int columnIndex) {
                switch (columnIndex) {
                case 0:
                    return String.class;
                case 1:
                    return Double.class;
                case 2:
                    return Long.class;
                default:
                    throw new AssertionError("Unknown column index");
                }
            }
        };

        ThermostatTable profileTable = new ThermostatTable(tableModel);

        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                profileListPane, profileTable.wrap());
        splitPane.setDividerLocation(SPLIT_PANE_RATIO);
        splitPane.setResizeWeight(0.5);

        return splitPane;
    }

    @Override
    public void addProfileActionListener(ActionListener<ProfileAction> listener) {
        listeners.add(listener);
    }

    @Override
    public void removeProfileActionlistener(ActionListener<ProfileAction> listener) {
        listeners.remove(listener);
    }

    private void fireProfileAction(final ProfileAction action) {
        if (!SwingUtilities.isEventDispatchThread()) {
            throw new AssertionError("Not in the EDT!");
        }

        new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                try {
                    ActionEvent<ProfileAction> event = new ActionEvent<>(this, action);
                    for (ActionListener<ProfileAction> listener : listeners) {
                        listener.actionPerformed(event);
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
                return null;
            }
        }.execute();
    }

    @Override
    public void setProfilingState(final ProfilingState profilingState) {
        final String status, buttonLabel;
        if (!viewControlsEnabled) {
            status = translator.localize(LocaleResources.PROFILER_CURRENT_STATUS_DEAD).getContents();
            buttonLabel = translator.localize(LocaleResources.START_PROFILING).getContents();
        } else if (profilingState == ProfilingState.STOPPING || profilingState == ProfilingState.STARTED) {
            status = translator.localize(LocaleResources.PROFILER_CURRENT_STATUS_ACTIVE).getContents();
            buttonLabel = translator.localize(LocaleResources.STOP_PROFILING).getContents();
        } else {
            status = translator.localize(LocaleResources.PROFILER_CURRENT_STATUS_INACTIVE).getContents();
            buttonLabel = translator.localize(LocaleResources.START_PROFILING).getContents();
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                currentStatusLabel.setText(status);
                if (!viewControlsEnabled) {
                    toggleButton.setToggleActionState(ProfilingState.DISABLED);
                } else {
                    toggleButton.setToggleActionState(profilingState);
                }
                toggleButton.setText(buttonLabel);
            }
        });
    }

    @Override
    public void setViewControlsEnabled(boolean enabled) {
        this.viewControlsEnabled = enabled;
        if (!enabled) {
            setProfilingState(ProfilingState.DISABLED);
        }
    }

    @Override
    public void setAvailableProfilingRuns(final List<Profile> data) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                DefaultListModel<Profile> listModel = (DefaultListModel<Profile>) profileList.getModel();
                updateSwingModel(data, listModel);
            }

            /**
             * Update the swing model based on the provided data, adding new
             * items and removing no-longer-present items from the swing model
             */
            private void updateSwingModel(final List<Profile> data, DefaultListModel<Profile> model) {
                for (Profile profile : data) {
                    if (!model.contains(profile)) {
                        model.addElement(profile);
                    }
                }

                List<Profile> toRemove = new ArrayList<>();
                Enumeration<Profile> e = model.elements();
                while (e.hasMoreElements()) {
                    Profile profile = e.nextElement();
                    if (!data.contains(profile)) {
                        toRemove.add(profile);
                    }
                }

                for (Profile profile : toRemove) {
                    model.removeElement(profile);
                }
            }
        });
    }

    @Override
    public Profile getSelectedProfile() {
        try {
            return new EdtHelper().callAndWait(new Callable<Profile>() {
                @Override
                public Profile call() throws Exception {
                    if (profileList.isSelectionEmpty()) {
                        throw new AssertionError("Selection is empty");
                    }
                    return profileList.getSelectedValue();
                }
            });
        } catch (InvocationTargetException | InterruptedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public void setProfilingDetailData(final ProfilingResult results) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                // delete all existing data
                tableModel.setRowCount(0);

                if (results.getMethodInfo().size() == 0) {
                    String noResultsMessage = translator
                            .localize(LocaleResources.PROFILER_NO_RESULTS)
                            .getContents();
                    tableModel.addRow(new Object[] { noResultsMessage, null, null });
                    return;
                }

                for (MethodInfo methodInfo: results.getMethodInfo()) {
                    Object[] data = new Object[] {
                            syntaxHighlightMethod(methodInfo.decl),
                            methodInfo.percentageTime,
                            methodInfo.totalTimeInMillis,
                    };
                    tableModel.addRow(data);
                }
            }
        });
    }

    private String syntaxHighlightMethod(MethodDeclaration decl) {
        final Color METHOD_COLOR = Palette.PALE_RED.getColor();
        final Color PARAMETER_COLOR = Palette.AZUREUS.getColor();
        final Color RETURN_TYPE_COLOR = Palette.SKY_BLUE.getColor();

        String highlightedName = htmlColorText(decl.getName(), METHOD_COLOR);
        String highlightedReturnType = htmlColorText(decl.getReturnType(), RETURN_TYPE_COLOR);

        StringBuilder toReturn = new StringBuilder();
        toReturn.append("<html>");
        toReturn.append("<pre>");

        toReturn.append(highlightedReturnType);
        toReturn.append(" ");
        toReturn.append("<b>");
        toReturn.append(highlightedName);
        toReturn.append("</b>");
        toReturn.append("(");

        ArrayList<String> parameters = new ArrayList<>();
        for (String parameter : decl.getParameters()) {
            parameters.add(htmlColorText(parameter, PARAMETER_COLOR));
        }

        toReturn.append(StringUtils.join(",", parameters));

        toReturn.append(")");
        toReturn.append("</pre>");
        toReturn.append("<html>");
        return toReturn.toString();

    }

    private String htmlColorText(String unescapedText, Color color) {
        return "<font color='" + ("#" + Integer.toHexString(color.getRGB() & 0x00ffffff)) + "'>"
                + StringUtils.htmlEscape(unescapedText) + "</font>";
    }

    @Override
    public Component getUiComponent() {
        return mainContainer;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame window = new JFrame();
                SwingVmProfileView view = new SwingVmProfileView();
                window.add(view.getUiComponent());
                window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                window.pack();
                window.setVisible(true);

                List<MethodInfo> data = new ArrayList<>();
                data.add(new MethodInfo(new MethodDeclaration("foo", list("int"), "int"), 1000, 1.0));
                data.add(new MethodInfo(new MethodDeclaration("bar", list("foo.bar.Baz", "int"), "Bar"), 100000, 100));
                ProfilingResult results = new ProfilingResult(data);
                view.setProfilingDetailData(results);
            }

            private List<String> list(String... args) {
                return Arrays.asList(args);
            }
        });
    }

}