cdc.perfs.ui.swing.ChartPanel Maven / Gradle / Ivy
package cdc.perfs.ui.swing;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import cdc.perfs.Context;
import cdc.perfs.Environment;
import cdc.perfs.EnvironmentKind;
import cdc.perfs.Measure;
import cdc.perfs.MeasureStatus;
import cdc.perfs.Position;
import cdc.perfs.ui.PerfsChartHelper;
import cdc.perfs.ui.PerfsChartHelper.ChangeListener;
import cdc.perfs.ui.RefreshRate;
import cdc.perfs.ui.Rendering;
import cdc.util.time.RefTime;
import cdc.util.time.TimeMeasureMode;
import cdc.util.time.TimeUnit;
/**
* Panel dedicated to graphical display of measures.
*
* @author Damien Carbonne
*
*/
final class ChartPanel extends JPanel {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LogManager.getLogger(ChartPanel.class);
protected final transient DragParams dragParams = new DragParams();
protected final Timer timer;
private RefreshRate refreshRate = null;
private Rendering rendering = null;
private boolean displayStatsEnabled = false;
protected boolean drawBordersEnabled = false;
protected boolean mouseOver = false;
protected long lastMouseMoveTime = -1;
/**
* Action that is triggered on timer events for refresh.
*/
private final transient ActionListener timerHandler;
protected final ContextsTableModel contextsModel;
final transient Helper helper;
public ChartPanel(ContextsTableModel model) {
super();
this.helper = new Helper(model.getEnvironment().getKind());
this.timerHandler = e -> {
helper.computeParameters();
repaint();
if (mouseOver) {
showToolTip(ChartPanel.this, e.getWhen());
}
};
this.contextsModel = model;
if (model.getEnvironment().getKind() == EnvironmentKind.RUNTIME) {
this.timer = new Timer(20, this.timerHandler);
} else {
this.timer = null;
this.contextsModel.addTableModelListener(event -> repaintIfSnapshot());
}
addMouseWheelListener(e -> {
final double ratio = e.getWheelRotation() < 0 ? 1.0 / 1.1 : 1.1;
setScale(helper.getScale() * ratio);
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
dragParams.enabled = helper.isFocusLocked() && (e.getButton() == MouseEvent.BUTTON1);
dragParams.x0 = e.getX();
dragParams.px = dragParams.x0;
}
@Override
public void mouseReleased(MouseEvent e) {
// Ignore
}
@Override
public void mouseEntered(MouseEvent e) {
mouseOver = true;
}
@Override
public void mouseExited(MouseEvent e) {
mouseOver = false;
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (dragParams.enabled) {
final int dx = e.getX() - dragParams.px;
dragParams.px = e.getX();
setFocusNanos(helper.getFocusNanos() + helper.deltaXToDeltaTime(-dx));
}
}
@Override
public void mouseMoved(MouseEvent e) {
helper.pick(e.getX(), e.getY());
lastMouseMoveTime = e.getWhen();
}
});
addHierarchyListener(e -> {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
if (isShowing()) {
repaintIfSnapshot();
if (timer != null) {
timer.restart();
}
} else {
if (timer != null) {
timer.stop();
}
}
}
});
addAncestorListener(new AncestorListener() {
@Override
public void ancestorRemoved(AncestorEvent event) {
// Ignore
}
@Override
public void ancestorMoved(AncestorEvent event) {
// Called when chart is scrolled
repaintIfSnapshot();
}
@Override
public void ancestorAdded(AncestorEvent event) {
// Ignore
}
});
setRefreshRate(RefreshRate.LOW);
setRendering(Rendering.FASTEST);
setPreferredSize(new Dimension(100, 1000));
if (timer != null) {
timer.start();
} else {
repaintIfSnapshot();
}
}
/**
* Blends a color with a fixed color (black).
*
* For a Runtime environment, result equals input color.
* For Snapshot environment, result is different.
*
* @param kind The environment kind.
* @param c The color to blend.
* @return The blended color.
*/
protected static Color blend(EnvironmentKind kind,
Color c) {
if (kind == EnvironmentKind.RUNTIME) {
return c;
} else {
final double w0 = 0.9;
final double w1 = 1.0 - w0;
final double r = w0 * c.getRed() + w1 * 0.0;
final double g = w0 * c.getGreen() + w1 * 0.0;
final double b = w0 * c.getBlue() + w1 * 0.0;
return new Color((int) r, (int) g, (int) b, 255);
}
}
private class Helper extends PerfsChartHelper {
protected final Rectangle2D.Double rect = new Rectangle2D.Double();
protected final Line2D line = new Line2D.Double();
private final Color bgStatsColor;
private final Color bgScaleColor;
private final Color bgColor;
private final Color aliveEvenBackgroundColor;
private final Color deadEvenBackgroundColor;
private final Color aliveOddBackgroundColor;
private final Color deadOddBackgroundColor;
private final Color fgColor;
private final Color runningColor;
private final Color frozenColor;
private final Color errorColor;
protected final Color borderColor;
protected final Color synthesisColor;
protected final Color infoColor;
private final Color centerTimeColor;
private final Color maxTimeColor;
final Rectangle visibleRectangle = new Rectangle();
final Filter rootFilter = new Filter(1);
Filter currentFilter = null;
Helper(EnvironmentKind kind) {
bgStatsColor = new Color(0.0f, 0.0f, 0.0f, 0.5f);
bgScaleColor = blend(kind, Color.WHITE);
bgColor = blend(kind, new Color(0.95f, 0.95f, 0.95f));
aliveEvenBackgroundColor = blend(kind, new Color(0.95f, 0.95f, 0.95f));
deadEvenBackgroundColor = blend(kind, new Color(0.95f, 0.85f, 0.85f));
aliveOddBackgroundColor = blend(kind, new Color(0.85f, 0.85f, 0.85f));
deadOddBackgroundColor = blend(kind, new Color(0.85f, 0.75f, 0.75f));
fgColor = blend(kind, new Color(0.6f, 0.6f, 0.6f));
runningColor = blend(kind, new Color(0.6f, 1.0f, 0.2f));
frozenColor = blend(kind, new Color(1.0f, 1.0f, 0.4f));
errorColor = blend(kind, new Color(1.0f, 0.6f, 0.55f));
borderColor = blend(kind, new Color(0.6f, 0.6f, 0.6f));
synthesisColor = blend(kind, Color.GRAY);
infoColor = Color.BLACK;
centerTimeColor = Color.GREEN;
maxTimeColor = Color.RED;
}
@Override
protected void retrieveWindowSize() {
final Dimension d = ChartPanel.this.getSize();
width = d.width;
height = d.height;
if (width < 0.01) {
width = 600.0;
}
}
@Override
protected void setPreferredHeight(int height) {
if (getPreferredSize().getHeight() != height) {
setPreferredSize(new Dimension(100, height));
revalidate();
}
}
@Override
public Environment getEnvironment() {
return contextsModel.getEnvironment();
}
@Override
protected void setToolTipTextHtml(String text) {
ChartPanel.this.setToolTipText(text);
}
@Override
protected List getContexts() {
return contextsModel.getContexts();
}
@Override
protected boolean isVisible(Context context) {
return contextsModel.isVisible(context);
}
@Override
protected void redraw() {
ChartPanel.this.repaint();
}
/**
* Draw time scale on top of visible part of the window.
*
* @param g2 The Graphic context to use.
*/
protected void drawScale(Graphics2D g2) {
final int y0 = (int) visibleRectangle.getMinY();
// Background
g2.setPaint(bgScaleColor);
rect.setRect(0, y0, width, DY0);
g2.fill(rect);
// Line under the area
line.setLine(0.0, y0 + DY0, timeToX(timeSupNanos), y0 + DY0);
g2.setPaint(fgColor);
g2.draw(line);
// Vertical time graduations
g2.setPaint(fgColor);
double x = Math.floor(timeInfNanos / timeStepNanos) * timeStepNanos;
assert (x <= timeInfNanos);
final Line2D line = new Line2D.Double();
for (; x <= timeSupNanos; x += timeStepNanos) {
final double xp = timeToX(x);
line.setLine(xp, y0 + DY0 - 5.0, xp, y0 + DY0);
g2.draw(line);
g2.drawString(RefTime.nanosToString((long) x, timeStepUnit),
(int) xp - shift(timeStepUnit),
y0 + 10);
}
}
/**
* Draw background elements.
*
* @param g2 The Graphic context to use.
*/
protected void drawBackground(Graphics2D g2) {
// Contexts background
double y0 = DY0;
int count = 0;
for (final Context context : contextsModel.getContexts()) {
if (contextsModel.isVisible(context)) {
final double dy = context.getHeight() * (DY1 + DY2) + DY2;
if (count % 2 == 0) {
if (context.isAlive()) {
g2.setPaint(aliveEvenBackgroundColor);
} else {
g2.setPaint(deadEvenBackgroundColor);
}
} else {
if (context.isAlive()) {
g2.setPaint(aliveOddBackgroundColor);
} else {
g2.setPaint(deadOddBackgroundColor);
}
}
rect.setRect(0.0, y0, width, dy);
g2.fill(rect);
y0 += dy;
count++;
}
}
// Remaining space
g2.setPaint(bgColor);
rect.setRect(0, y0, width, height - DY0);
g2.fill(rect);
// Vertical time lines
g2.setPaint(fgColor);
double x = Math.floor(timeInfNanos / timeStepNanos) * timeStepNanos;
assert (x <= timeInfNanos);
for (; x <= timeSupNanos; x += timeStepNanos) {
final double xp = timeToX(x);
line.setLine(xp, DY0, xp, y0);
g2.draw(line);
}
// Elapsed time vertical line
if (timeInfNanos <= elapsedNanos && elapsedNanos <= timeSupNanos) {
g2.setPaint(maxTimeColor);
final double xp = timeToX(elapsedNanos);
line.setLine(xp, DY0 - 5.0, xp, y0);
g2.draw(line);
}
// Focus time vertical line
if (timeInfNanos <= focusNanos && focusNanos <= timeSupNanos) {
g2.setPaint(centerTimeColor);
final double xp = timeToX(focusNanos);
line.setLine(xp, DY0 - 5.0, xp, y0);
g2.draw(line);
}
}
/**
* Draw all visible contexts.
*
* @param g2 the graphical 2D context.
*/
protected void drawContexts(Graphics2D g2) {
double y0 = DY0 + DY2;
for (final Context context : contextsModel.getContexts()) {
if (contextsModel.isVisible(context)) {
drawContext(g2, context, y0);
y0 += context.getHeight() * (DY1 + DY2);
// Line under the context area
line.setLine(0.0, y0, timeToX(timeSupNanos), y0);
g2.setPaint(fgColor);
g2.draw(line);
y0 += DY2;
}
}
}
class Filter extends AbstractFilter {
private Graphics2D g2;
public Filter(int depth) {
super(depth);
}
@Override
protected AbstractFilter create(int depth) {
return new Filter(depth);
}
@Override
protected void drawMeasure(Measure measure,
double y) {
Helper.this.drawMeasure(g2, measure, y);
}
@Override
public Filter next() {
return (Filter) super.next();
}
@Override
protected void drawSynthesis() {
addDrawing();
setRectSafe(rect, x0, y, x1 - x0, DY1);
// Fill rectangle
g2.setPaint(synthesisColor);
g2.fill(rect);
// Draw border
if (drawBordersEnabled) {
g2.setPaint(borderColor);
g2.draw(rect);
}
// Draw infos
if (x1 - x0 > 100.0 && x0 <= getWidth() && x0 > -1000.0) {
g2.setPaint(infoColor);
g2.drawString(count + " measures", (int) x0 + 2, (int) (y + DY1 - 2.0));
}
}
public void init(Graphics2D g2,
double y) {
super.init(y);
this.g2 = g2;
}
}
/**
* Draw one context.
*
* @param g2 the graphical 2D context.
* @param context the context to draw.
* @param y0 vertical position of the first measure to draw.
*/
private void drawContext(Graphics2D g2,
Context context,
double y0) {
// Index of the first root measure to draw
int index = context.getRootMeasureIndex((long) timeInfNanos,
TimeMeasureMode.RELATIVE,
Position.GREATER_OR_EQUAL);
final int count = context.getRootMeasuresCount();
rootFilter.init(g2, y0);
currentFilter = rootFilter;
while (index >= 0 && index < count) {
final Measure measure = context.getRootMeasure(index);
final double t0 = measure.getRelativeBeginNanos();
final double t1 = measure.getRelativeEndOrCurrentNanos();
if (t0 <= timeSupNanos && t1 >= timeInfNanos) {
rootFilter.addMeasure(measure);
index++;
} else {
// Measures are ordered: next measures are not visible
index = -1;
}
}
rootFilter.flush();
}
protected void setRectSafe(Rectangle2D.Double r,
double x,
double y,
double w,
double h) {
final double eps = 5.0;
final double dx0 = (x <= -eps ? -eps - x : 0.0);
final double dx1 = (x + w > width + eps ? x + w - width - eps : 0.0);
r.setRect(x + dx0, y, w - dx0 - dx1, h);
}
/**
* Recursively draw one measure and its children.
*
* @param g2 the graphical 2D context.
* @param measure the measure to draw.
* @param y vertical position of the measure to draw.
*/
protected void drawMeasure(Graphics2D g2,
Measure measure,
double y) {
addDrawing();
final MeasureStatus status = measure.getStatus();
final double x0 = getX0(measure);
final double x1 = getX1(measure);
switch (status) {
case FROZEN:
g2.setPaint(frozenColor);
break;
case FROZEN_ON_ERROR:
g2.setPaint(errorColor);
break;
case RUNNING:
g2.setPaint(runningColor);
break;
default:
break;
}
final double dx = x1 - x0;
setRectSafe(rect, x0, y, dx, DY1);
// Fill rectangle
g2.fill(rect);
// Draw border
if (drawBordersEnabled) {
g2.setPaint(borderColor);
g2.draw(rect);
}
// Draw information
if (dx > 100.0 && x0 <= width && x1 >= 50.0) {
g2.setPaint(infoColor);
g2.drawString(measure.getLabel(), Math.max(0, (int) x0 + 2), (int) (y + DY1 - 2.0));
}
// Draw children
if (measure.getFirstChild() != null) {
final Filter mem = currentFilter;
final Filter filter = currentFilter.next();
filter.init(g2, y + DY1 + DY2);
currentFilter = filter;
for (Measure child = measure.getFirstChild(); child != null; child = child.getNextSibling()) {
filter.addMeasure(child);
}
filter.flush();
currentFilter = mem;
}
}
protected void drawStats(Graphics2D g2) {
if (getDisplayStatsEnabled()) {
final boolean runtime = getEnvironment().getKind() == EnvironmentKind.RUNTIME;
final int dx = 150;
final int dy = 70 + (runtime ? 20 : 0);
final int xmargin = 10;
final int ymargin = 20 + (int) visibleRectangle.getMinY();
g2.setPaint(bgStatsColor);
rect.setRect(width - dx - xmargin, ymargin, dx, dy);
g2.fill(rect);
g2.setPaint(Color.WHITE);
g2.drawString(RefTime.nanosToString(getDrawingNanos()), (int) width - dx, ymargin + 20);
g2.drawString(getDrawingCount() + " item(s)", (int) width - dx, ymargin + 40);
g2.drawString(getEnvironment().getMeasuresCount() + " measure(s)", (int) width - dx, ymargin + 60);
if (runtime) {
g2.drawString(String.format("%03d ms", (int) RefTime.nanosToDouble(getTickNanos(), TimeUnit.MILLI)), (int) width - dx, ymargin + 80);
}
}
}
}
@Override
public void paint(Graphics g) {
helper.startDrawing();
helper.visibleRectangle.setBounds(getVisibleRect());
super.paint(g);
final Graphics2D g2 = (Graphics2D) g;
switch (getRendering()) {
case BEST:
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
break;
case FASTEST:
default:
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_SPEED);
break;
}
helper.computeParameters();
helper.drawBackground(g2);
helper.drawContexts(g2);
helper.drawScale(g2);
helper.endDrawing();
helper.drawStats(g2);
}
protected static class DragParams {
boolean enabled;
/** Current x0. */
int x0;
/** Previous x0. */
int px;
}
protected void showToolTip(JComponent component,
long time) {
final ToolTipManager manager = ToolTipManager.sharedInstance();
if (time - lastMouseMoveTime > manager.getInitialDelay()) {
final PointerInfo info = MouseInfo.getPointerInfo();
if (info == null) {
LOGGER.warn("Can not retrieve pointer info");
} else {
final Point absLocation = info.getLocation();
final Point relLocation = new Point(absLocation);
SwingUtilities.convertPointFromScreen(relLocation, component);
helper.pick(relLocation.x, relLocation.y);
if (component.contains(relLocation)) {
final MouseEvent event =
new MouseEvent(component, MouseEvent.MOUSE_MOVED,
time, 0,
relLocation.x, relLocation.y,
absLocation.x, absLocation.y,
0, false, 0);
manager.mouseMoved(event);
}
}
}
}
public Environment getEnvironment() {
return contextsModel.getEnvironment();
}
protected List getContexts() {
return contextsModel.getContexts();
}
protected boolean isVisible(Context context) {
return contextsModel.isVisible(context);
}
public final void addChangeListener(ChangeListener listener) {
helper.addChangeListener(listener);
}
public final void removeChangeListener(ChangeListener listener) {
helper.removeChangeListener(listener);
}
public void setFocusLocked(boolean locked) {
helper.setFocusLocked(locked);
repaint();
}
public boolean isFocusLocked() {
return helper.isFocusLocked();
}
public void setScale(double scale) {
helper.setScale(scale);
}
public double getScale() {
return helper.getScale();
}
public void setFocusNanos(double time) {
helper.setFocusNanos(time);
}
public double getFocusNanos() {
return helper.getFocusNanos();
}
public void incrementFocus(long nanosSinceFirstChange) {
helper.incrementFocus(nanosSinceFirstChange);
}
public void decrementFocus(long nanosSinceFirstChange) {
helper.decrementFocus(nanosSinceFirstChange);
}
public void setRefreshRate(RefreshRate rate) {
this.refreshRate = rate;
if (this.timer != null) {
this.timer.setDelay(rate.getDelay());
}
helper.fireChange();
repaintIfSnapshot();
}
public RefreshRate getRefreshRate() {
return refreshRate;
}
public void setRendering(Rendering rendering) {
this.rendering = rendering;
helper.fireChange();
repaintIfSnapshot();
}
public Rendering getRendering() {
return rendering;
}
public void setDisplayStatsEnabled(boolean enabled) {
this.displayStatsEnabled = enabled;
helper.fireChange();
repaintIfSnapshot();
}
public boolean getDisplayStatsEnabled() {
return displayStatsEnabled;
}
public void setDrawBordersEnabled(boolean enabled) {
this.drawBordersEnabled = enabled;
helper.fireChange();
repaintIfSnapshot();
}
public boolean getDrawBordersEnabled() {
return drawBordersEnabled;
}
protected void repaintIfSnapshot() {
if (getEnvironment().getKind() == EnvironmentKind.SNAPSHOT) {
repaintIfShowing();
}
}
protected void repaintIfShowing() {
if (isShowing()) {
helper.computeParameters();
helper.redraw();
}
}
}