diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 76bddc77..27046c14 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1 @@ -* @ritvikrao -* @lvkale -* @matthiasdiener +* @ritvikrao @lvkale diff --git a/.gitignore b/.gitignore index d5b774c8..77bd67c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .gradle/ bin/projections.jar build/ + +# Written by Projections next to any opened trace when a window is closed +*.projrc + +# macOS +.DS_Store diff --git a/src/projections/Tools/TimeProfile/TimeProfileWindow.java b/src/projections/Tools/TimeProfile/TimeProfileWindow.java index 8b7622ea..e3ca92a9 100644 --- a/src/projections/Tools/TimeProfile/TimeProfileWindow.java +++ b/src/projections/Tools/TimeProfile/TimeProfileWindow.java @@ -27,6 +27,8 @@ import projections.gui.GenericGraphWindow; import projections.gui.IntervalChooserPanel; import projections.gui.JPanelToImage; +import projections.gui.Legend; +import projections.gui.graph.Graph; import projections.gui.MainWindow; import projections.gui.RangeDialog; import projections.gui.U; @@ -65,6 +67,19 @@ public class TimeProfileWindow extends GenericGraphWindow private JCheckBox showMarkersCheckBox; private JCheckBox analyzeSlopesCheckBox; private JCheckBox hideMouseoversCheckBox; + private JCheckBox showLegendCheckBox; + private JCheckBox labelRegionsCheckBox; + + private static final int LEGEND_TOP_N = 10; + private Legend legendWindow; + + // Regions must sustain at least this utilization share (percentage + // points) for at least 1/20 of the displayed intervals to get an + // on-chart label. + private static final double REGION_LABEL_MIN_PERCENT = 25.0; + private static final int REGION_LABEL_MIN_FRACTION = 20; + // EPs currently labeled on the chart, omitted from the compact legend + private Set overlaidEPs = new HashSet(); private long intervalSize; private int startInterval; @@ -176,6 +191,16 @@ private void createLayout() { hideMouseoversCheckBox.setToolTipText("Disable the displaying of information associated with the data under the mouse pointer."); hideMouseoversCheckBox.addActionListener(this); + showLegendCheckBox = new JCheckBox("Show Legend (top " + LEGEND_TOP_N + ")"); + showLegendCheckBox.setSelected(false); + showLegendCheckBox.setToolTipText("Movable window listing the " + LEGEND_TOP_N + " largest activities in the displayed range; drag it over an empty part of the chart. The Legend menu shows the full list."); + showLegendCheckBox.addActionListener(this); + + labelRegionsCheckBox = new JCheckBox("Label Regions"); + labelRegionsCheckBox.setSelected(false); + labelRegionsCheckBox.setToolTipText("Draw entry method names directly on large single-color regions of the chart; labeled entries are then omitted from the compact legend."); + labelRegionsCheckBox.addActionListener(this); + controlPanel = new JPanel(); controlPanel.setLayout(gbl); // Util.gblAdd(controlPanel, epSelection, gbc, 0,0, 1,1, 0,0); @@ -183,6 +208,8 @@ private void createLayout() { Util.gblAdd(controlPanel, showMarkersCheckBox, gbc, 3,0, 1,1, 0,0); Util.gblAdd(controlPanel, analyzeSlopesCheckBox, gbc, 4,0, 1,1, 0,0); Util.gblAdd(controlPanel, hideMouseoversCheckBox, gbc, 5,0, 1,1, 0,0); + Util.gblAdd(controlPanel, showLegendCheckBox, gbc, 6,0, 1,1, 0,0); + Util.gblAdd(controlPanel, labelRegionsCheckBox, gbc, 7,0, 1,1, 0,0); JPanel graphPanel = getMainPanel(); Util.gblAdd(mainPanel, graphPanel, gbc, 0,0, 1,1, 1,1); @@ -198,11 +225,14 @@ private static class SortableEPs implements Comparable{ private double value; private String name; private Paint paint; + // utilization-weighted mean interval; orders phase-like EPs left-to-right + private double centroid; - private SortableEPs(double value, String name, Paint paint){ + private SortableEPs(double value, String name, Paint paint, double centroid){ this.value = value; this.name = name; this.paint = paint; + this.centroid = centroid; } public int compareTo(Object o) { @@ -218,31 +248,129 @@ else if(other.value > value) } private void generateLegend(boolean useShortenedNames){ + makeLegend("Legend", useShortenedNames, Integer.MAX_VALUE, false, true, false, false); + } + + /** Recompute and apply (or clear) the on-chart region labels. */ + private void updateRegionLabels() { + overlaidEPs.clear(); + if (!labelRegionsCheckBox.isSelected() || graphData == null) { + graphCanvas.setRegionLabels(null); + return; + } + List labels = new ArrayList(); + int numIntervals = graphData.length; + int minRun = Math.max(2, numIntervals / REGION_LABEL_MIN_FRACTION); + // Only real entry methods get labels; Idle and Overhead colors are + // fixed and familiar to viewers. + for (int ep = 0; ep < numEPs; ep++) { + if (!stateArray[ep]) { + continue; + } + int runStart = -1; + for (int i = 0; i <= numIntervals; i++) { + boolean inRegion = i < numIntervals && graphData[i][ep] >= REGION_LABEL_MIN_PERCENT; + if (inRegion && runStart < 0) { + runStart = i; + } else if (!inRegion && runStart >= 0) { + int runEnd = i - 1; + if (runEnd - runStart + 1 >= minRun) { + int mid = (runStart + runEnd) / 2; + // Vertical center of this EP's band in the stacked bar: + // EPs below it in the stack are those with smaller index + double yBottom = 0; + for (int under = 0; under < ep; under++) { + if (stateArray[under]) { + yBottom += graphData[mid][under]; + } + } + labels.add(new Graph.RegionLabel(runStart, runEnd, + yBottom + graphData[mid][ep] / 2.0, + MainWindow.runObject[myRun].getPrettyEntryNameByIndex(ep), + MainWindow.runObject[myRun].getEPColorMap()[ep])); + overlaidEPs.add(ep); + } + runStart = -1; + } + } + } + graphCanvas.setRegionLabels(labels); + } + + /** Open (or refresh) the movable top-N legend controlled by the checkbox. */ + private void showLegendWindow() { + if (graphData == null) { + return; + } + java.awt.Point oldLocation = null; + if (legendWindow != null) { + oldLocation = legendWindow.getFrame().getLocation(); + legendWindow.dispose(); + } + legendWindow = makeLegend("Legend (top " + LEGEND_TOP_N + ")", true, LEGEND_TOP_N, true, false, true, true); + if (legendWindow == null) { + return; + } + if (oldLocation != null) { + legendWindow.getFrame().setLocation(oldLocation); + } else { + legendWindow.getFrame().setLocationRelativeTo(thisWindow); + } + // Keep the checkbox in sync if the user closes the legend window directly + legendWindow.getFrame().addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent e) { + legendWindow = null; + showLegendCheckBox.setSelected(false); + } + }); + } + + private void closeLegendWindow() { + if (legendWindow != null) { + Legend l = legendWindow; + legendWindow = null; + l.dispose(); + } + } + + private Legend makeLegend(String title, boolean useShortenedNames, int maxEntries, boolean showPercent, boolean includeIdleOverhead, boolean omitOverlaid, boolean orderByAppearance){ List l = new ArrayList(); // Accumulate data shown in graph double[] sums = new double[numEPs+2]; + double[] firstMoment = new double[numEPs+2]; double grandTotal = 0.0; for(int i=0; i 0 ? firstMoment[ep] / sums[ep] : 0; + } // Put data into list for (int ep=0; ep names = new ArrayList(); List paints = new ArrayList(); + List selected = new ArrayList(); Iterator iter = l.iterator(); - while(iter.hasNext()){ + while(iter.hasNext() && selected.size() < maxEntries){ SortableEPs s = iter.next(); if(s.value > grandTotal * 0.005){ - names.add(s.name); - paints.add(s.paint); + selected.add(s); } } + if(orderByAppearance){ + // Left-to-right chart order for phase-like entry methods + Collections.sort(selected, new Comparator() { + public int compare(SortableEPs a, SortableEPs b) { + return Double.compare(a.centroid, b.centroid); + } + }); + } + + for(SortableEPs s : selected){ + if(showPercent) + names.add(String.format("%.1f%% %s", s.value * 100.0 / grandTotal, s.name)); + else + names.add(s.name); + paints.add(s.paint); + } + + if (names.isEmpty()) { + return null; + } // Display the legend - new Legend("Legend", names, paints); + return new Legend(title, names, paints); } @@ -506,6 +654,10 @@ else if( MainWindow.runObject[myRun].hasSumFiles()){ } public void done() { setOutputGraphData(); + updateRegionLabels(); + if (showLegendCheckBox.isSelected()) { + showLegendWindow(); + } thisWindow.setVisible(true); } }; @@ -651,6 +803,17 @@ public void actionPerformed(ActionEvent e) { graphCanvas.showMarkers(showMarkersCheckBox.isSelected()); } else if (e.getSource() == hideMouseoversCheckBox) { graphCanvas.showBubble(! hideMouseoversCheckBox.isSelected()); + } else if (e.getSource() == showLegendCheckBox) { + if (showLegendCheckBox.isSelected()) { + showLegendWindow(); + } else { + closeLegendWindow(); + } + } else if (e.getSource() == labelRegionsCheckBox) { + updateRegionLabels(); + if (showLegendCheckBox.isSelected()) { + showLegendWindow(); // re-filter against the labeled EPs + } } else if (e.getSource() == setRanges) { showDialog(); } else if(e.getSource() == mDisplayLegend){ diff --git a/src/projections/Tools/TimeProfile/Legend.java b/src/projections/gui/Legend.java similarity index 92% rename from src/projections/Tools/TimeProfile/Legend.java rename to src/projections/gui/Legend.java index fa4f196c..60aa0847 100644 --- a/src/projections/Tools/TimeProfile/Legend.java +++ b/src/projections/gui/Legend.java @@ -1,4 +1,4 @@ -package projections.Tools.TimeProfile; +package projections.gui; @@ -15,12 +15,10 @@ import javax.swing.JFrame; import javax.swing.JLabel; -import projections.gui.JPanelToImage; -import projections.gui.MainWindow; - /** Display a legend in a new window (clickable to save image to file) */ -class Legend implements MouseListener { +public class Legend implements MouseListener { private BufferedImage image; + private JFrame frame; private Paint fgColor; private Paint bgColor; @@ -34,7 +32,7 @@ class Legend implements MouseListener { private Font namesFont; private Font legendFont; - Legend(String title, List names, List paints){ + public Legend(String title, List names, List paints){ this.names = names; namesFont = new Font("SansSerif", Font.PLAIN, fontSizeNames() ); @@ -87,7 +85,7 @@ class Legend implements MouseListener { // Display the thing ImageIcon imageIcon = new ImageIcon(image); - JFrame f = new JFrame(); + JFrame f = new JFrame(title); JLabel l = new JLabel(imageIcon); @@ -95,11 +93,20 @@ class Legend implements MouseListener { f.getContentPane().add(l); f.pack(); f.setVisible(true); + frame = f; g.dispose(); } + public JFrame getFrame() { + return frame; + } + + public void dispose() { + frame.dispose(); + } + private int fontSizeLegend(){ return 30; } diff --git a/src/projections/gui/ProfileWindow.java b/src/projections/gui/ProfileWindow.java index 6fddbf13..d49f5b90 100644 --- a/src/projections/gui/ProfileWindow.java +++ b/src/projections/gui/ProfileWindow.java @@ -11,7 +11,11 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Enumeration; +import java.util.List; import java.util.Stack; import java.util.Vector; @@ -67,6 +71,9 @@ class ProfileWindow extends ProjectionsWindow private int displayPanelTabIndex; private JCheckBox chkEnableGrid; + private JCheckBox chkShowLegend; + private static final int LEGEND_TOP_N = 10; + private Legend legendWindow; private JButton btnIncX, btnDecX, btnResX, btnIncY, btnDecY, btnResY, btnExportToFile; private JFloatTextField txtScaleX, txtScaleY; @@ -151,6 +158,16 @@ private void CreateLayout(){ chkEnableGrid.addActionListener(this); Util.gblAdd(gridPanel, chkEnableGrid, gbc, 0, 0, 1, 1, 0, 0); + JPanel legendPanel = new JPanel(); + legendPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Legend")); + legendPanel.setLayout(gbl); + + chkShowLegend = new JCheckBox("Top " + LEGEND_TOP_N); + chkShowLegend.setSelected(false); + chkShowLegend.setToolTipText("Movable window listing the " + LEGEND_TOP_N + " largest activities by average utilization; drag it over an empty part of the chart."); + chkShowLegend.addActionListener(this); + Util.gblAdd(legendPanel, chkShowLegend, gbc, 0, 0, 1, 1, 0, 0); + //create x-y scale panel JPanel xScalePanel = new JPanel(); xScalePanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "x-scale")); @@ -196,11 +213,12 @@ private void CreateLayout(){ Container wholeContainer = getContentPane(); wholeContainer.setLayout(gbl); - Util.gblAdd(wholeContainer, displayPanel, gbc, 0,0, 3,1, 1,1, 5,5,5,5); + Util.gblAdd(wholeContainer, displayPanel, gbc, 0,0, 4,1, 1,1, 5,5,5,5); Util.gblAdd(wholeContainer, gridPanel, gbc, 0,1, 1,1, 1,0, 2,2,2,2); - Util.gblAdd(wholeContainer, xScalePanel, gbc, 1,1, 1,1, 5,0, 2,2,2,2); - Util.gblAdd(wholeContainer, yScalePanel, gbc, 2,1, 1,1, 5,0, 2,2,2,2); + Util.gblAdd(wholeContainer, legendPanel, gbc, 1,1, 1,1, 1,0, 2,2,2,2); + Util.gblAdd(wholeContainer, xScalePanel, gbc, 2,1, 1,1, 5,0, 2,2,2,2); + Util.gblAdd(wholeContainer, yScalePanel, gbc, 3,1, 1,1, 5,0, 2,2,2,2); } public void showDialog(){ @@ -231,9 +249,14 @@ public void actionPerformed(ActionEvent evt){ // clean current slate float scaleX = 0; float scaleY = 0; - if (evt.getSource() instanceof JCheckBox) { - JCheckBox chk = (JCheckBox) evt.getSource(); - displayCanvas.setGridEnabled(chk.isSelected()); + if (evt.getSource() == chkEnableGrid) { + displayCanvas.setGridEnabled(chkEnableGrid.isSelected()); + } else if (evt.getSource() == chkShowLegend) { + if (chkShowLegend.isSelected()) { + showLegendWindow(); + } else { + closeLegendWindow(); + } } if (evt.getSource() instanceof JButton) { JButton b = (JButton) evt.getSource(); @@ -329,6 +352,81 @@ public void stateChanged(ChangeEvent e){ + /** Open (or refresh) the movable top-N legend controlled by the checkbox. + * Entries are ranked by average utilization over the selected PEs + * (matching the Avg bar). IDLE is omitted: its color is fixed and + * familiar to viewers. */ + private void showLegendWindow() { + if (avgData == null) { + return; + } + int numEPs = MainWindow.runObject[myRun].getNumUserEntries(); + final float[] value = new float[numEPs + NUM_SYS_EPS]; + for (int i = 0; i < value.length; i++) { + value[i] = avgData[0][i] + avgData[1][i]; + } + List ranked = new ArrayList(); + for (int i = 0; i < value.length; i++) { + if (i == numEPs + 2) { + continue; // IDLE + } + if (value[i] > thresh) { + ranked.add(i); + } + } + Collections.sort(ranked, new Comparator() { + public int compare(Integer a, Integer b) { + return Float.compare(value[b], value[a]); + } + }); + + List names = new ArrayList(); + List paints = new ArrayList(); + for (int k = 0; k < ranked.size() && k < LEGEND_TOP_N; k++) { + int ep = ranked.get(k); + String name; + if (ep == numEPs) { + name = "PACKING"; + } else if (ep == numEPs + 1) { + name = "UNPACKING"; + } else { + name = MainWindow.runObject[myRun].getPrettyEntryNameByIndex(ep); + } + names.add(String.format("%.1f%% %s", value[ep], name)); + paints.add(colors[ep]); + } + if (names.isEmpty()) { + return; + } + + java.awt.Point oldLocation = null; + if (legendWindow != null) { + oldLocation = legendWindow.getFrame().getLocation(); + legendWindow.dispose(); + } + legendWindow = new Legend("Legend (top " + LEGEND_TOP_N + ")", names, paints); + if (oldLocation != null) { + legendWindow.getFrame().setLocation(oldLocation); + } else { + legendWindow.getFrame().setLocationRelativeTo(this); + } + // Keep the checkbox in sync if the user closes the legend window directly + legendWindow.getFrame().addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent e) { + legendWindow = null; + chkShowLegend.setSelected(false); + } + }); + } + + private void closeLegendWindow() { + if (legendWindow != null) { + Legend l = legendWindow; + legendWindow = null; + l.dispose(); + } + } + private void showChangeColorDialog() { new ChooseEntriesWindow(this); } @@ -430,6 +528,10 @@ private void setDisplayProfileData(){ displayCanvas.setYAxis("Usage Percent %"); displayCanvas.setDisplayDataSource(dataSource, colorMap, colors, nameMap); displayCanvas.repaint(); + + if (chkShowLegend != null && chkShowLegend.isSelected()) { + showLegendWindow(); + } } private void createDisplayDataSource(){ diff --git a/src/projections/gui/graph/Graph.java b/src/projections/gui/graph/Graph.java index f1c8692c..85b4425b 100644 --- a/src/projections/gui/graph/Graph.java +++ b/src/projections/gui/graph/Graph.java @@ -120,6 +120,26 @@ private double maxvalueY(){ private boolean showMarkers = false; + /** A text label drawn on top of a horizontal run of bars/intervals. + * Used by tools to name large single-color regions in stacked graphs. */ + public static class RegionLabel { + public final double startIndex; // first bin of the run + public final double endIndex; // last bin of the run (inclusive) + public final double yValue; // vertical center, in data source units + public final String text; + public final java.awt.Paint regionPaint; // color under the label, for contrast + + public RegionLabel(double startIndex, double endIndex, double yValue, String text, java.awt.Paint regionPaint) { + this.startIndex = startIndex; + this.endIndex = endIndex; + this.yValue = yValue; + this.text = text; + this.regionPaint = regionPaint; + } + } + + private java.util.List regionLabels = null; + /** Special construgraphCanvasctor. This can only be called from a projections tool!!! */ public Graph() @@ -199,6 +219,12 @@ public void setMarkers(TreeMap phaseMarkers){ repaint(); } + /** Set (or clear, with null) labels drawn over large regions of the graph. */ + public void setRegionLabels(java.util.List labels){ + this.regionLabels = labels; + repaint(); + } + public void setData(DataSource d, XAxis x, YAxis y) { @@ -472,6 +498,7 @@ private void drawDisplay(Graphics2D g) drawYAxis(g); drawMarkers(g); + drawRegionLabels(g); } @@ -530,6 +557,57 @@ private void drawMarkers(Graphics2D g) { } + private void drawRegionLabels(Graphics2D g) { + if (regionLabels == null || regionLabels.isEmpty()) { + return; + } + Font regionFont = new Font("SansSerif", Font.BOLD, 12); + g.setFont(regionFont); + FontMetrics fm = g.getFontMetrics(regionFont); + Object oldHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + for (RegionLabel label : regionLabels) { + double widthPx = (label.endIndex - label.startIndex + 1) * pixelincrementX(); + // Truncate the text if the region is narrow; skip if almost nothing fits + String text = label.text; + while (text.length() > 3 && fm.stringWidth(text) > widthPx * 0.95) { + text = text.substring(0, text.length() - 2) + "\u2026"; + } + if (fm.stringWidth(text) > widthPx * 0.95) { + continue; + } + + int cx = originX() + (int)(((label.startIndex + label.endIndex) / 2.0 + 0.5) * pixelincrementX()); + int cy = originY() - (int)(label.yValue * pixelincrementY()); + if (cy > originY() - fm.getHeight()/2 || cy < topMargin() + fm.getHeight()/2) { + continue; + } + int tx = cx - fm.stringWidth(text) / 2; + int ty = cy + fm.getAscent() / 2; + + // Black text on light regions, white on dark ones + g.setColor(contrastingTextColor(label.regionPaint)); + g.drawString(text, tx, ty); + } + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHint); + } + + /** Black or white, whichever contrasts with the given paint. */ + private static Color contrastingTextColor(java.awt.Paint p) { + Color c = null; + if (p instanceof Color) { + c = (Color) p; + } else if (p instanceof java.awt.GradientPaint) { + c = ((java.awt.GradientPaint) p).getColor1(); + } + if (c == null) { + return Color.black; + } + double luminance = 0.299*c.getRed() + 0.587*c.getGreen() + 0.114*c.getBlue(); + return luminance < 128 ? Color.white : Color.black; + } + private void drawXAxis(Graphics2D g) { g.setColor(MainWindow.runObject[myRun].foreground);