cdc.perfs.ui.fx.ChartPane Maven / Gradle / Ivy
package cdc.perfs.ui.fx;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.util.Date;
import java.util.List;
import cdc.perfs.core.Context;
import cdc.perfs.core.Environment;
import cdc.perfs.core.EnvironmentKind;
import cdc.perfs.core.Measure;
import cdc.perfs.core.MeasureStatus;
import cdc.perfs.core.Position;
import cdc.perfs.core.SpanPosition;
import cdc.perfs.ui.PerfsChartHelper;
import cdc.perfs.ui.PerfsChartHelper.ChangeListener;
import cdc.perfs.ui.RefreshRate;
import cdc.perfs.ui.Rendering;
import cdc.perfs.ui.fx.ContextsTableModel.Record;
import cdc.util.time.RefTime;
import cdc.util.time.TimeMeasureMode;
import cdc.util.time.TimeUnit;
import javafx.animation.AnimationTimer;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
final class ChartPane extends BorderPane {
final Helper helper;
protected final ContextsTableModel contextsModel;
protected final DragParams dragParams = new DragParams();
protected double boundsWidth = 0.0;
protected double boundsHeight = 0.0;
protected double visibleMinY = 0.0;
protected final Timer timer;
private RefreshRate refreshRate = null;
private Rendering rendering = null;
private boolean displayStatsEnabled = false;
protected boolean drawBordersEnabled = false;
protected final Canvas wCanvas = new Canvas(100.0, 200.0);
protected boolean mouseOver = false;
protected long lastMouseMoveTime = -1;
private final EventHandler zoomHandler;
public ChartPane(ContextsTableModel model) {
super();
if (model == null) {
throw new IllegalArgumentException();
}
this.helper = new Helper(model.getEnvironment().getKind());
this.zoomHandler = event -> {
final double ratio = event.getDeltaY() > 0.0 ? 1.0 / 1.1 : 1.1;
setScale(helper.getScale() * ratio);
event.consume();
};
wCanvas.setOnMousePressed(event -> {
dragParams.enabled = helper.isFocusLocked() && (event.getButton() == MouseButton.PRIMARY);
dragParams.x0 = (int) event.getX();
dragParams.px = dragParams.x0;
});
wCanvas.setOnMouseDragged(event -> {
if (dragParams.enabled) {
final int dx = (int) event.getX() - dragParams.px;
dragParams.px = (int) event.getX();
setFocusNanos(helper.getFocusNanos() + helper.deltaXToDeltaTime(-dx));
}
});
wCanvas.setOnMouseMoved(event -> {
helper.setToolTip(helper.pick((int) event.getX(), (int) event.getY()));
lastMouseMoveTime = System.nanoTime();
});
wCanvas.setOnMouseEntered(event -> mouseOver = true);
wCanvas.setOnMouseExited(event -> mouseOver = false);
wCanvas.setOnScroll(zoomHandler);
wCanvas.addEventFilter(ScrollEvent.ANY, zoomHandler);
final InvalidationListener listener = o -> draw();
wCanvas.widthProperty().addListener(listener);
wCanvas.heightProperty().addListener(listener);
final Group wGroup = new Group();
wGroup.getChildren().add(wCanvas);
setCenter(wGroup);
this.contextsModel = model;
if (model.getEnvironment().getKind() == EnvironmentKind.RUNTIME) {
this.timer = new Timer();
timer.start();
} else {
this.timer = null;
this.contextsModel.asObservableList().addListener((ListChangeListener) change -> repaintIfSnapshot());
}
setRefreshRate(RefreshRate.LOW);
setRendering(Rendering.FASTEST);
}
protected static double snap(double x) {
return ((int) x) + 0.5;
}
protected static double round(double x) {
return (int) x;
}
protected static Color blend(EnvironmentKind kind,
Color c) {
if (kind == EnvironmentKind.RUNTIME) {
return c;
} else {
final double w0 = 0.7;
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(r, g, b, 1.0);
}
}
private class Helper extends PerfsChartHelper {
protected final Rectangle2D.Double rect = new Rectangle2D.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;
private final Tooltip wTooltip = new Tooltip();
final Filter rootFilter = new Filter(1);
Filter currentFilter = null;
public 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.95, 0.95, 0.95, 1.0));
aliveEvenBackgroundColor = blend(kind, new Color(0.95, 0.95, 0.95, 1.0));
deadEvenBackgroundColor = blend(kind, new Color(0.95, 0.85, 0.85, 1.0));
aliveOddBackgroundColor = blend(kind, new Color(0.85, 0.85, 0.85, 1.0));
deadOddBackgroundColor = blend(kind, new Color(0.85, 0.75, 0.75, 1.0));
fgColor = blend(kind, new Color(0.6f, 0.6, 0.6, 1.0));
runningColor = blend(kind, new Color(0.6, 1.0, 0.2, 1.0));
frozenColor = blend(kind, new Color(1.0, 1.0, 0.4, 1.0));
errorColor = blend(kind, new Color(1.0, 0.6, 0.55, 1.0));
borderColor = blend(kind, new Color(0.6, 0.6, 0.6, 1.0));
synthesisColor = blend(kind, Color.GRAY);
infoColor = Color.BLACK;
centerTimeColor = new Color(0.0, 1.0, 0.0, 1.0);
maxTimeColor = new Color(1.0, 0.0, 0.0, 1.0);
Tooltip.install(wCanvas, wTooltip);
}
@Override
protected void retrieveWindowSize() {
width = wCanvas.getWidth();
height = wCanvas.getHeight();
}
@Override
protected void setPreferredHeight(int height) {
final int h = Math.max(height, (int) boundsHeight);
if (getPrefHeight() != h) {
setPrefHeight(h);
wCanvas.setHeight(h);
draw();
}
}
@Override
public Environment getEnvironment() {
return contextsModel.getEnvironment();
}
@Override
protected void setToolTipTextHtml(String text) {
// Ignore
assert false;
}
private Text newText(String text,
String style) {
final Text result = new Text(text);
result.setStyle(style);
return result;
}
@Override
public void setToolTip(Helper.PickedData data) {
if (data.context == null) {
wTooltip.setText("");
wTooltip.setContentDisplay(ContentDisplay.TEXT_ONLY);
} else {
final VBox wVBox = new VBox();
final String normalStyle = "-fx-font-weight: normal;";
final String boldStyle = "-fx-font-weight: bold;";
final Text wContextName = newText(data.context.getName(), normalStyle);
final Text wContextAlive = newText(data.context.isAlive() ? " ALIVE" : " DEAD", boldStyle);
wVBox.getChildren().add(new HBox(wContextName, wContextAlive));
if (data.measure != null) {
// TODO does not work
final String errorStyle = "-fx-text-fill: red; -fx-font-weight: bold;";
// Label and level
final Text wLabelLevel = newText("\n[" + data.measure.getLabel() + "] " + data.measure.getLevel().name(), boldStyle);
wVBox.getChildren().add(wLabelLevel);
// Depth and height
final Text wDepthLabel = newText("depth: ", normalStyle);
final Text wDepth = newText(Integer.toString(data.measure.getDepth()), boldStyle);
final Text wHeightLabel = newText(" height: ", normalStyle);
final Text wHeight = newText(Integer.toString(data.measure.getHeight()), boldStyle);
wVBox.getChildren().add(new HBox(wDepthLabel, wDepth, wHeightLabel, wHeight));
// Start and date
final Text wStart = newText("start: " + RefTime.nanosToString(data.measure.getRelativeBeginNanos()), normalStyle);
final Text wDate = newText(PerfsChartHelper.toString(Date.from(RefTime.REF_INSTANT)), boldStyle);
wVBox.getChildren().add(new HBox(wStart, wDate));
// elapsed
final Text wElapsedLabel = newText("elapsed: ", normalStyle);
final Text wElapsed = newText(RefTime.nanosToString(data.measure.getElapsedNanos()), boldStyle);
wVBox.getChildren().add(new HBox(wElapsedLabel, wElapsed));
// status
final Text wStatus = newText(data.measure.getStatus().name(),
data.measure.getStatus() == MeasureStatus.FROZEN_ON_ERROR ? errorStyle : boldStyle);
final HBox wHBox = new HBox(wStatus);
wHBox.setAlignment(Pos.CENTER);
wVBox.getChildren().add(wHBox);
}
wTooltip.setGraphic(wVBox);
// TODO does not always work
wTooltip.setStyle("-fx-background-color: #FFFFDD;");
wTooltip.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
}
@Override
protected List getContexts() {
return contextsModel.getContexts();
}
@Override
protected boolean isVisible(Context context) {
return contextsModel.isVisible(context);
}
@Override
protected void redraw() {
draw();
}
protected void drawScale(GraphicsContext gc) {
final double y0 = visibleMinY;
// Background
gc.setFill(bgScaleColor);
gc.fillRect(0.0, round(y0), width, DY0);
// Line under the area
gc.setStroke(fgColor);
gc.strokeLine(0.0, snap(y0 + DY0), timeRelNanosToX(timeSupRelNanos), snap(y0 + DY0));
// Vertical time graduations
gc.setFill(fgColor);
double x = Math.floor(timeInfRelNanos / timeStepNanos) * timeStepNanos;
assert (x <= timeInfRelNanos);
for (; x <= timeSupRelNanos; x += timeStepNanos) {
final double xp = snap(timeRelNanosToX(x));
gc.strokeLine(xp, y0 + DY0 - 5.0, xp, y0 + DY0);
gc.fillText(RefTime.nanosToString((long) x, timeStepUnit),
xp - shift(timeStepUnit),
y0 + 10.0,
1000.0);
}
}
protected void drawBackground(GraphicsContext gc) {
// 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()) {
gc.setFill(aliveEvenBackgroundColor);
} else {
gc.setFill(deadEvenBackgroundColor);
}
} else {
if (context.isAlive()) {
gc.setFill(aliveOddBackgroundColor);
} else {
gc.setFill(deadOddBackgroundColor);
}
}
gc.fillRect(0.0, y0, width, dy);
y0 += dy;
count++;
}
}
// Remaining space
gc.setFill(bgColor);
gc.fillRect(0, y0, width, height - DY0);
// Vertical time lines
gc.setStroke(fgColor);
double x = Math.floor(timeInfRelNanos / timeStepNanos) * timeStepNanos;
assert (x <= timeInfRelNanos);
for (; x <= timeSupRelNanos; x += timeStepNanos) {
final double xp = snap(timeRelNanosToX(x));
gc.strokeLine(xp, DY0, xp, y0);
}
// Elapsed time vertical line
if (timeInfRelNanos <= elapsedNanos
&& elapsedNanos <= timeSupRelNanos
&& super.isFocusLocked()) {
gc.setStroke(maxTimeColor);
final double xp = snap(timeRelNanosToX(elapsedNanos));
gc.strokeLine(xp, DY0 - 5.0, xp, y0);
}
// Focus time vertical line
if (timeInfRelNanos <= focusRelNanos && focusRelNanos <= timeSupRelNanos) {
gc.setStroke(centerTimeColor);
final double xp = snap(timeRelNanosToX(focusRelNanos));
gc.strokeLine(xp, DY0 - 5.0, xp, y0);
}
}
protected void drawContexts(GraphicsContext gc) {
double y0 = DY0 + DY2;
for (final Context context : contextsModel.getContexts()) {
if (contextsModel.isVisible(context)) {
drawContext(gc, context, y0);
y0 += context.getHeight() * (DY1 + DY2);
// Line under the context area
gc.setStroke(fgColor);
gc.strokeLine(0.0, snap(y0), timeRelNanosToX(timeSupRelNanos), snap(y0));
y0 += DY2;
}
}
}
class Filter extends AbstractFilter {
private GraphicsContext gc;
public Filter(int depth) {
super(depth);
}
@Override
protected AbstractFilter create(int depth) {
return new Filter(depth);
}
@Override
protected void drawMeasure(Measure measure,
SpanPosition position,
double y) {
Helper.this.drawMeasure(gc, measure, position, y);
}
@Override
public Filter next() {
return (Filter) super.next();
}
@Override
protected void drawSynthesis() {
addDrawing();
setRectSafe(rect, x0, y, x1 - x0, DY1);
// Fill rectangle
gc.setFill(synthesisColor);
gc.fillRect(round(rect.x), round(rect.y), round(rect.width), rect.height);
// Draw border
if (drawBordersEnabled) {
gc.setStroke(borderColor);
gc.strokeRect(snap(rect.x), snap(rect.y), round(rect.width), rect.height);
}
// Draw infos
if (x1 - x0 > 100.0 && x0 <= getWidth() && x0 > -1000.0) {
gc.setFill(infoColor);
gc.fillText(count + " measures", (int) x0 + 2.0, (int) (y + DY1 - 2.0));
}
}
public void init(GraphicsContext gc,
double y) {
super.init(y);
this.gc = gc;
}
}
/**
* Draw one context (and all its measures).
*
* @param gc The graphical 2D context.
* @param context The context.
* @param y0 The vertical position.
*/
private void drawContext(GraphicsContext gc,
Context context,
double y0) {
// Index of the first root measure to draw
int index = context.getRootMeasureIndex((long) timeInfRelNanos,
TimeMeasureMode.RELATIVE,
Position.GREATER_OR_EQUAL);
final int count = context.getRootMeasuresCount();
rootFilter.init(gc, 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 <= timeSupRelNanos && t1 >= timeInfRelNanos) {
final SpanPosition position = rootFilter.getPosition(measure);
rootFilter.addMeasure(measure, position);
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 gc The graphical 2D context.
* @param measure The measure to draw.
* @param position The measure span position.
* @param y Vertical position of the measure to draw.
*/
protected void drawMeasure(GraphicsContext gc,
Measure measure,
SpanPosition position,
double y) {
addDrawing();
final MeasureStatus status = measure.getStatus();
final double x0 = getX0(measure);
final double x1 = getX1(measure);
switch (status) {
case FROZEN:
gc.setFill(frozenColor);
break;
case FROZEN_ON_ERROR:
gc.setFill(errorColor);
break;
case RUNNING:
gc.setFill(runningColor);
break;
default:
break;
}
final double dx = x1 - x0;
setRectSafe(rect, x0, y, dx, DY1);
// Fill rectangle
gc.fillRect(round(rect.x), round(rect.y), round(rect.width), rect.height);
// Draw border
if (drawBordersEnabled) {
gc.setStroke(borderColor);
gc.strokeRect(snap(rect.x), snap(rect.y), round(rect.width), rect.height);
}
// Draw information
if (dx > 100.0 && x0 <= width && x1 >= 50.0) {
gc.setFill(infoColor);
gc.fillText(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(gc, y + DY1 + DY2);
currentFilter = filter;
for (Measure child = measure.getFirstChild(); child != null; child = child.getNextSibling()) {
final SpanPosition childPosition = position == SpanPosition.INSIDE
? SpanPosition.INSIDE
: filter.getPosition(child);
filter.addMeasure(child, childPosition);
}
filter.flush();
currentFilter = mem;
}
}
protected void drawStats(GraphicsContext gc) {
if (getDisplayStatsEnabled()) {
final double dx = 150.0;
final double dy = 70.0;
final double xmargin = 10.0;
final double ymargin = 20.0 + visibleMinY;
gc.setFill(bgStatsColor);
gc.fillRect(width - dx - xmargin, ymargin, dx, dy);
gc.setFill(Color.WHITE);
gc.fillText(RefTime.nanosToString(getDrawingNanos()), (int) width - dx, ymargin + 20);
gc.fillText(getDrawingCount() + " item(s)", (int) width - dx, ymargin + 40);
gc.fillText(String.format("%03d ms", (int) RefTime.nanosToDouble(getTickNanos(), TimeUnit.MILLI)), (int) width - dx, ymargin + 60);
}
}
}
protected static class DragParams {
boolean enabled;
/** Current x0. */
int x0;
/** Previous x0. */
int px;
}
private class Timer extends AnimationTimer {
private long previous = 0;
public Timer() {
super();
}
@Override
public void handle(long now) {
if (now - previous > getRefreshRate().getDelay() * 1000000) {
previous = now;
repaintIfShowing();
}
}
}
protected void showToolTip(Node node,
long time) {
if (time - lastMouseMoveTime > 0) { // initial delay ?
final Point mouse = MouseInfo.getPointerInfo().getLocation();
final Point2D local = wCanvas.screenToLocal(mouse.getX(), mouse.getY());
helper.setToolTip(helper.pick((int) local.getX(), (int) local.getY()));
// if (node.contains(relLocation)) {
// final MouseEvent event =
// new MouseEvent(node, MouseEvent.MOUSE_MOVED,
// time, 0,
// relLocation.x, relLocation.y,
// absLocation.x, absLocation.y,
// 0, false, 0);
// manager.mouseMoved(event);
// }
}
}
protected void draw() {
final GraphicsContext gc = wCanvas.getGraphicsContext2D();
helper.startDrawing();
helper.computeParameters();
helper.drawBackground(gc);
helper.drawContexts(gc);
helper.drawScale(gc);
helper.endDrawing();
helper.drawStats(gc);
}
public void setViewportBounds(double width,
double height,
double minY) {
boundsWidth = width;
boundsHeight = height;
visibleMinY = minY;
wCanvas.setWidth(width);
draw();
}
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);
draw();
}
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) {
if (rate != refreshRate) {
this.refreshRate = rate;
if (this.timer != null) {
// TODO
}
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 (getScene() != null
&& getScene().getWindow().isShowing()) {
helper.computeParameters();
helper.redraw();
if (mouseOver) {
showToolTip(ChartPane.this, System.nanoTime());
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy