All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cdc.perfs.ui.fx.ChartPane Maven / Gradle / Ivy

There is a newer version: 0.52.0
Show newest version
package cdc.perfs.ui.fx;

import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

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.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.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(Context context,
                               Measure measure) {
            if (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(context.getName(), normalStyle);
                final Text wContextAlive = newText(context.isAlive() ? " ALIVE" : " DEAD", boldStyle);
                wVBox.getChildren().add(new HBox(wContextName, wContextAlive));

                if (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[" + measure.getLabel() + "] " + measure.getLevel().name(), boldStyle);
                    wVBox.getChildren().add(wLabelLevel);

                    // Depth and height
                    final Text wDepthLabel = newText("depth: ", normalStyle);
                    final Text wDepth = newText(Integer.toString(measure.getDepth()), boldStyle);
                    final Text wHeightLabel = newText(" height: ", normalStyle);
                    final Text wHeight = newText(Integer.toString(measure.getHeight()), boldStyle);
                    wVBox.getChildren().add(new HBox(wDepthLabel, wDepth, wHeightLabel, wHeight));

                    // Start and date
                    final Text wStart = newText("start: " + RefTime.nanosToString(measure.getRelativeBeginNanos()), normalStyle);

                    final Date date = Date.from(RefTime.REF_INSTANT);
                    calendar.setTime(date);
                    final String s = String.format(" (%02d:%02d:%02d.%03d)",
                                                   calendar.get(Calendar.HOUR_OF_DAY),
                                                   calendar.get(Calendar.MINUTE),
                                                   calendar.get(Calendar.SECOND),
                                                   calendar.get(Calendar.MILLISECOND));

                    final Text wDate = newText(s, boldStyle);

                    wVBox.getChildren().add(new HBox(wStart, wDate));

                    // elapsed
                    final Text wElapsedLabel = newText("elapsed: ", normalStyle);
                    final Text wElapsed = newText(RefTime.nanosToString(measure.getElapsedNanos()), boldStyle);
                    wVBox.getChildren().add(new HBox(wElapsedLabel, wElapsed));

                    // status
                    final Text wStatus = newText(measure.getStatus().name(),
                                                 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.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