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

cdc.perfs.ui.PerfsChartHelper Maven / Gradle / Ivy

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

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import cdc.perfs.Context;
import cdc.perfs.Environment;
import cdc.perfs.Measure;
import cdc.perfs.MeasureStatus;
import cdc.util.lang.ArraysUtil;
import cdc.util.time.Chronometer;
import cdc.util.time.RefTime;
import cdc.util.time.Ticker;
import cdc.util.time.TimeMeasureMode;
import cdc.util.time.TimeUnit;

/**
 * Base class used to define a Perfs chart.
 *
 * @author Damien Carbonne
 *
 */
public abstract class PerfsChartHelper {
    private final List listeners = new ArrayList<>();

    public static final double MIN_SCALE = 1.0E1;
    public static final double MAX_SCALE = 1.0E12;

    /**
     * Current display scale.
     * Must be in range [MIN_SCALE, MAX_SCALE].
     */
    private double scale = 1000.0;

    /**
     * Base value for the computation of currentPixelPerNano (for scale = 1.0).
     */
    protected static final double BASE_PIXELS_PER_NANO = 10.0;

    /**
     * Pixels per Nano ratio.
     * Computed as: BASE_PIXELS_PER_NANO / scale.
     */
    protected double pixelsPerNano;

    /**
     * Current focus (relative) time.
     * When focusLocked is false, focus time = elapsed time.
     * Otherwise, initialized at lock time to lock time, then modified by user.
     */
    protected double focusNanos = 0.0;

    /**
     * Is focus time locked?
     */
    protected boolean focusLocked = false;

    /** Current elapsed time. */
    protected double elapsedNanos;

    /** Width of the window in pixels. */
    protected double width;

    /** Height of the window in pixels. */
    protected double height;

    /**
     * Time span of the window.
     * Computed as: width * pixelsPerNano.
     * This holds: timeSpanNanos = timeSupNanos - timeInfNanos.
     */
    protected double timeSpanNanos;

    /**
     * Unit the must be used to display time step.
     */
    protected TimeUnit timeStepUnit;

    /** Min time value of the window. */
    protected double timeInfNanos;

    /** Max time value of the window. */
    protected double timeSupNanos;

    /** Interval between two time graduations */
    protected double timeStepNanos;

    /** Height for first rectangle */
    protected static final double DY0 = 20.0;
    /** Height of each rectangle */
    protected static final double DY1 = 15.0;
    /** Height between two rectangles. */
    protected static final double DY2 = 5.0;

    protected final Calendar calendar = new GregorianCalendar();
    private final Chronometer drawingChrono = new Chronometer();
    private int drawingCount = 0;

    private final Ticker ticker = new Ticker();

    private static final double NANO = 1.0;
    private static final double MICRO = 1.0e3;
    private static final double MILLI = 1.0e6;
    private static final double SECOND = 1.0e9;
    private static final double MINUTE = 60.0 * SECOND;
    private static final double HOUR = 60.0 * MINUTE;

    /**
     * Sorted array of time intervals (nanos) accepted for graduations.
     */
    private static final double[] TIME_STEPS_NANOS = {
            1.0 * NANO,
            2.0 * NANO,
            5.0 * NANO,
            10.0 * NANO,
            20.0 * NANO,
            50.0 * NANO,
            100.0 * NANO,
            200.0 * NANO,
            500.0 * NANO,

            1.0 * MICRO,
            2.0 * MICRO,
            5.0 * MICRO,
            10.0 * MICRO,
            20.0 * MICRO,
            50.0 * MICRO,
            100.0 * MICRO,
            200.0 * MICRO,
            500.0 * MICRO,

            1.0 * MILLI,
            2.0 * MILLI,
            5.0 * MILLI,
            10.0 * MILLI,
            20.0 * MILLI,
            50.0 * MILLI,
            100.0 * MILLI,
            200.0 * MILLI,
            500.0 * MILLI,

            1.0 * SECOND,
            2.0 * SECOND,
            5.0 * SECOND,
            10.0 * SECOND,
            20.0 * SECOND,
            30.0 * SECOND,

            1.0 * MINUTE,
            2.0 * MINUTE,
            5.0 * MINUTE,
            10.0 * MINUTE,
            20.0 * MINUTE,
            30.0 * MINUTE,

            1.0 * HOUR,
            3.0 * HOUR,
            6.0 * HOUR,
            12.0 * HOUR,
            24.0 * HOUR
    };

    private static final String HTML_COLOR_BACKGROUND = "#FFFFDD";
    private static final String HTML_COLOR_LABEL = "#777777";
    private static final String HTML_COLOR_INFO = "#000000";
    private static final String HTML_COLOR_ERROR = "#FF0000";

    public static final int SCALE_MAX = 1000;

    private static final double BETA =
            SCALE_MAX / (Math.log10(PerfsChartHelper.MAX_SCALE) - Math.log10(PerfsChartHelper.MIN_SCALE));
    private static final double ALPHA =
            Math.log10(PerfsChartHelper.MAX_SCALE) * BETA;

    private static TimeUnit getUnit(double nanos) {
        if (nanos >= HOUR) {
            return TimeUnit.HOUR;
        } else if (nanos >= MINUTE) {
            return TimeUnit.MINUTE;
        } else if (nanos >= SECOND) {
            return TimeUnit.SECOND;
        } else if (nanos >= MILLI) {
            return TimeUnit.MILLI;
        } else if (nanos >= MICRO) {
            return TimeUnit.MICRO;
        } else {
            return TimeUnit.NANO;
        }
    }

    /**
     * Value to be used to center text of time graduation.
     *
     * @param unit Unit used to draw graduation text.
     * @return An estimation of the half horizontal size of the text.
     */
    protected static int shift(TimeUnit unit) {
        switch (unit) {
        case HOUR:
        case MINUTE:
            return 15;
        case SECOND:
            return 30;
        case MILLI:
            return 45;
        case MICRO:
        case NANO:
            return 55;
        default:
            return 0;
        }
    }

    private static double getStep(double nanos) {
        return ArraysUtil.smallestSaturatedGreaterOrEqual(TIME_STEPS_NANOS, nanos);
    }

    protected static String toHtml(String s) {
        final StringBuilder builder = new StringBuilder();
        for (int index = 0; index < s.length(); index++) {
            final char c = s.charAt(index);
            switch (c) {
            case '<':
                builder.append("<");
                break;
            case '>':
                builder.append(">");
                break;
            default:
                builder.append(c);
                break;
            }
        }

        return builder.toString();
    }

    @FunctionalInterface
    public static interface ChangeListener {
        public void processParamChange(PerfsChartHelper source);
    }

    public final void addChangeListener(ChangeListener listener) {
        if (listener != null && !listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    public final void removeChangeListener(ChangeListener listener) {
        listeners.remove(listener);
    }

    public final void fireChange() {
        for (final ChangeListener listener : listeners) {
            listener.processParamChange(this);
        }
    }

    /**
     * @return The current scale.
     */
    public final double getScale() {
        return scale;
    }

    /**
     * @return The elapsed time.
     */
    public final double getElapsedNanos() {
        return elapsedNanos;
    }

    /**
     * @return Focus time.
     */
    public final double getFocusNanos() {
        return focusNanos;
    }

    /**
     * Set current scale.
     * 

* This passed value is saturated to interval of supported scales. * * @param scale The scale to set. */ public final void setScale(double scale) { this.scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale)); computeParameters(); redraw(); } public static double toScale(long timeSpanNanos, double width) { return (BASE_PIXELS_PER_NANO / width) * timeSpanNanos; } /** * Set focus time. *

* This passed value is saturated to [0.0, getElapsedNanos()]. * * @param time The focus time to set. */ public final void setFocusNanos(double time) { focusNanos = Math.min(Math.max(0, time), elapsedNanos); computeParameters(); redraw(); } private double deltaTime(long nanosSinceFirstChange) { return (nanosSinceFirstChange / 1.0E9) * 5.0 / pixelsPerNano; } public final void incrementFocus(long nanosSinceFirstChange) { focusNanos += deltaTime(nanosSinceFirstChange); if (focusNanos > elapsedNanos) { focusNanos = elapsedNanos; } computeParameters(); redraw(); } public final void decrementFocus(long nanosSinceFirstChange) { focusNanos -= deltaTime(nanosSinceFirstChange); if (focusNanos < 0.0) { focusNanos = 0.0; } computeParameters(); redraw(); } /** * @return True when focus time is locked (user controlled). */ public final boolean isFocusLocked() { return focusLocked; } /** * Change locking of focus time. * When locked, focus time is user controlled. * At time of locking, focus time is set to elapsed time. * Later, it can be controlled by user. * When unlocked, focus time equals elapsed time. * * @param locked Shall focus time be locked? */ public final void setFocusLocked(boolean locked) { focusLocked = locked; computeParameters(); redraw(); } /** * Retrieve window size and update width and height */ protected abstract void retrieveWindowSize(); protected abstract void setPreferredHeight(int height); public abstract Environment getEnvironment(); public final void computeParameters() { // Retrieve window size retrieveWindowSize(); // Freeze elapsed time elapsedNanos = getEnvironment().getElapsedNanos(); // Compute time scale pixelsPerNano = BASE_PIXELS_PER_NANO / scale; timeSpanNanos = width / pixelsPerNano; // If focus is not locked,focus time equals elapsed time if (!focusLocked) { focusNanos = elapsedNanos; } // Compute inf and sup bounds of displayed time timeInfNanos = focusNanos - (timeSpanNanos / 2.0); if (timeInfNanos < 0) { timeInfNanos = 0; } timeSupNanos = timeInfNanos + timeSpanNanos; // Compute time interval between 2 time graduations // We want graduations to be approximately every 150 pixels (at least) // Time interval (in nanos) corresponding to exactly 150 pixels final double dt = 150.0 / pixelsPerNano; // Find the closest value, among the accepted ones, that is greater // than dt (to ensure more that 100 pixels) timeStepNanos = PerfsChartHelper.getStep(dt); timeStepUnit = PerfsChartHelper.getUnit(timeStepNanos); // Compute preferred height double preferredHeight = DY0; for (final Context context : getContexts()) { if (isVisible(context)) { final int depth = context.getHeight(); preferredHeight += depth * (DY1 + DY2) + DY2; } } setPreferredHeight((int) preferredHeight); fireChange(); } public final double getWidth() { return width; } public final double getHeight() { return height; } public final double timeToX(double t) { return (t - timeInfNanos) * pixelsPerNano; } public final double xToTime(double x) { return timeInfNanos + x / pixelsPerNano; } public final double getX0(Measure measure) { return timeToX(measure.getRelativeBeginNanos()); } public final double getX1(Measure measure) { return timeToX(measure.getRelativeEndOrCurrentNanos()); } public final double getDx(Measure measure) { return measure.getElapsedNanos() * pixelsPerNano; } public final double deltaXToDeltaTime(double dx) { return dx / pixelsPerNano; } protected abstract void setToolTipTextHtml(String text); /** * @return A list of contexts. */ protected abstract List getContexts(); /** * Returns {@code true} if a context is visible. * * @param context The context. * @return {@code true} if {@code context} is visible. */ protected abstract boolean isVisible(Context context); /** * Request a scene redraw. */ protected abstract void redraw(); private static void htmlStart(StringBuilder builder) { builder.append(""); builder.append(""); } private static void htmlStop(StringBuilder builder) { builder.append(""); builder.append(""); } public final String getToolTipTextHtml(Context context) { return getToolTipTextHtml(context, null); } private static void beginFont(StringBuilder builder, String color) { builder.append(""); } private static void endFont(StringBuilder builder) { builder.append(""); } private static void beginBold(StringBuilder builder) { builder.append(""); } private static void endBold(StringBuilder builder) { builder.append(""); } private static void addBreak(StringBuilder builder) { builder.append("
"); } private static void addAttribute(StringBuilder builder, String name, String value) { if (name != null) { beginFont(builder, HTML_COLOR_LABEL); builder.append(name); endFont(builder); } if (value != null) { beginFont(builder, HTML_COLOR_INFO); beginBold(builder); builder.append(value); endBold(builder); endFont(builder); } } public final String getToolTipTextHtml(Context context, Measure measure) { final StringBuilder builder = new StringBuilder(); htmlStart(builder); addAttribute(builder, context.getId() + ": " + context.getName(), context.isAlive() ? " ALIVE" : " DEAD"); addBreak(builder); addAttribute(builder, "measure(s): ", Integer.toString(context.getMeasuresCount())); addBreak(builder); addAttribute(builder, "height: ", Integer.toString(context.getHeight())); if (measure != null) { addBreak(builder); addBreak(builder); beginFont(builder, HTML_COLOR_INFO); beginBold(builder); builder.append("[" + toHtml(measure.getLabel()) + "] " + measure.getLevel()); addBreak(builder); endBold(builder); endFont(builder); addAttribute(builder, "depth: ", Integer.toString(measure.getDepth())); addAttribute(builder, " height: ", Integer.toString(measure.getHeight())); addBreak(builder); 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)); addAttribute(builder, "start: " + RefTime.nanosToString(measure.getRelativeBeginNanos()), " (" + s + ")"); addBreak(builder); addAttribute(builder, "elapsed: ", RefTime.nanosToString(measure.getElapsedNanos())); addBreak(builder); if (measure.getStatus() == MeasureStatus.FROZEN_ON_ERROR) { beginFont(builder, HTML_COLOR_ERROR); } else { beginFont(builder, HTML_COLOR_INFO); } endBold(builder); builder.append("

"); beginBold(builder); builder.append(measure.getStatus()); endBold(builder); endFont(builder); } htmlStop(builder); return builder.toString(); } public final void pick(int x, int y) { Context pickedContext = null; int pickedLevel = -1; double y0 = DY0; // start of the context layer double y1; // end of the context layer for (final Context context : getContexts()) { if (isVisible(context)) { final int depth = context.getHeight(); y1 = y0 + depth * (DY1 + DY2) + DY2; if (y >= y0 && y <= y1) { // y is in this context layer pickedContext = context; double yy0 = y0 + DY2; double yy1; for (int level = 0; level < depth; level++) { yy1 = yy0 + DY1; if (y >= yy0 && y <= yy1) { // y is at this level pickedLevel = level; } yy0 = yy1 + DY2; } } y0 = y1; } } final Object pickedItem; final Measure pickedMeasure; if (pickedLevel >= 0) { final double t = xToTime(x); final long epsilon = (long) (2.0 / this.pixelsPerNano); pickedMeasure = pickedContext.getMeasure((long) t, TimeMeasureMode.RELATIVE, epsilon, pickedLevel); } else { pickedMeasure = null; } if (pickedMeasure != null) { pickedItem = pickedMeasure; } else { pickedItem = pickedContext; } if (pickedItem == null) { setToolTip(null, null); } else { setToolTip(pickedContext, pickedMeasure); } } public void setToolTip(Context context, Measure measure) { if (context == null) { setToolTipTextHtml(null); } else if (measure == null) { setToolTipTextHtml(getToolTipTextHtml(context)); } else { setToolTipTextHtml(getToolTipTextHtml(context, measure)); } } public final void startDrawing() { drawingChrono.start(); drawingCount = 0; ticker.tick(); } public final void addDrawing() { drawingCount++; } public final void endDrawing() { drawingChrono.suspend(); } public final int getDrawingCount() { return drawingCount; } public final long getDrawingNanos() { return drawingChrono.getElapsedNanos(); } public final long getTickNanos() { return ticker.getLastPeriod(); } public static double sliderToScale(double value) { return Math.pow(10.0, (ALPHA - value) / BETA); } public static double scaleToSlider(double scale) { return ALPHA - BETA * Math.log10(scale); } /** * Helper class that can replace the drawing of many tiny rectangles by one rectangle. * * @author Damien Carbonne */ protected abstract class AbstractFilter { protected final int depth; protected double y; /** Number of drawn rectangles. */ protected int count = 0; /** Last non drawn measure. */ private Measure last = null; protected double x0; protected double x1; private AbstractFilter next = null; protected AbstractFilter(int depth) { this.depth = depth; } public AbstractFilter next() { if (next == null) { next = create(depth + 1); } return next; } protected abstract AbstractFilter create(int depth); /** * Draw a measure. * * @param measure The measure. * @param y The vertical position. */ protected abstract void drawMeasure(Measure measure, double y); /** * Draw a measure synthesis. *

* This corresponds to a rectangle starting vertically at {@code y} and horizontally * between {@code x0} and {@code x1}. */ protected abstract void drawSynthesis(); public final void init(double y) { this.y = y; } /** * Adds a measure. *

* This measure will either be drawn or merged with other measures (synthesis). * * @param measure The measure. */ public final void addMeasure(Measure measure) { if (measure.getStatus() == MeasureStatus.RUNNING || getDx(measure) >= 1.0) { // Always draw running measures or measures that are long // enough flush(); drawMeasure(measure, y); } else { final double mx0 = getX0(measure); final double mx1 = getX1(measure); if (count == 0) { // First measure is frozen and short x0 = mx0; x1 = mx1; last = measure; count++; } else { // Count >= 1 if (mx1 <= x1 + 1.0) { // Merge drawing of current measure with accumulated // ones x1 = mx1; last = measure; count++; } else { flush(); x0 = mx0; x1 = mx1; last = measure; count++; } } } } public final void flush() { // Flush existing drawing if (count == 1) { // One accumulated measure drawMeasure(last, y); } else if (count > 1) { // Synthesis drawSynthesis(); } count = 0; last = null; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy