cdc.perfs.ui.PerfsChartHelper Maven / Gradle / Ivy
package cdc.perfs.ui;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import cdc.perfs.core.Context;
import cdc.perfs.core.Environment;
import cdc.perfs.core.Measure;
import cdc.perfs.core.MeasureStatus;
import cdc.perfs.core.SpanPosition;
import cdc.util.lang.ArrayUtils;
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 focusRelNanos = 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 (relative) time value of the window. */
protected double timeInfRelNanos;
protected long timeInfAbsNanos;
/** Max (relative) time value of the window. */
protected double timeSupRelNanos;
protected long timeSupAbsNanos;
/** 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;
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 ArrayUtils.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 focusRelNanos;
}
/**
* 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) {
focusRelNanos = 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) {
focusRelNanos += deltaTime(nanosSinceFirstChange);
if (focusRelNanos > elapsedNanos) {
focusRelNanos = elapsedNanos;
}
computeParameters();
redraw();
}
public final void decrementFocus(long nanosSinceFirstChange) {
focusRelNanos -= deltaTime(nanosSinceFirstChange);
if (focusRelNanos < 0.0) {
focusRelNanos = 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) {
focusRelNanos = elapsedNanos;
}
// Compute inf and sup bounds of displayed time
timeInfRelNanos = focusRelNanos - (timeSpanNanos / 2.0);
if (timeInfRelNanos < 0) {
timeInfRelNanos = 0;
}
timeSupRelNanos = timeInfRelNanos + timeSpanNanos;
timeInfAbsNanos = (long) timeInfRelNanos + getEnvironment().getReferenceNanos();
timeSupAbsNanos = (long) timeSupRelNanos + getEnvironment().getReferenceNanos();
// 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 timeRelNanosToX(double t) {
return (t - timeInfRelNanos) * pixelsPerNano;
}
public final double xToTimeRelNanos(double x) {
return timeInfRelNanos + x / pixelsPerNano;
}
public final double getX0(Measure measure) {
return timeRelNanosToX(measure.getRelativeBeginNanos());
}
public final double getX1(Measure measure) {
return timeRelNanosToX(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("");
}
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 static String toString(Date date) {
final Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
return String.format("%02d:%02d:%02d.%03d",
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
calendar.get(Calendar.SECOND),
calendar.get(Calendar.MILLISECOND));
}
public static Date relativeNanosToDate(long nanos) {
return Date.from(RefTime.REF_INSTANT.plus(Duration.ofNanos(nanos)));
}
public static String getToolTipTextHtml(PickedData data) {
if (data == null || data.isVoid()) {
return null;
}
final StringBuilder builder = new StringBuilder();
htmlStart(builder);
addAttribute(builder,
data.context.getId() + ": " + data.context.getName(),
data.context.isAlive() ? " ALIVE" : " DEAD");
addBreak(builder);
addAttribute(builder,
"ref time: ",
toString(Date.from(RefTime.REF_INSTANT)));
addBreak(builder);
addAttribute(builder,
"min time: ",
RefTime.nanosToString(data.context.getMinRelativeBeginNanosOrCurrent())
+ " (" + toString(relativeNanosToDate(data.context.getMinRelativeBeginNanosOrCurrent())) + ")");
addBreak(builder);
addAttribute(builder,
"max time: ",
RefTime.nanosToString(data.context.getMaxRelativeEndNanosOrCurrent())
+ " (" + toString(relativeNanosToDate(data.context.getMaxRelativeEndNanosOrCurrent())) + ")");
addBreak(builder);
addAttribute(builder,
"measure(s): ",
String.format("%,d", data.context.getMeasuresCount()));
addBreak(builder);
addAttribute(builder,
"height: ",
String.format("%,d", data.context.getHeight()));
if (data.measure != null) {
addBreak(builder);
addBreak(builder);
addAttribute(builder,
data.measure.getLabel() + " ",
data.measure.getLevel().name());
addBreak(builder);
addAttribute(builder,
"depth: ",
String.format("%,d", data.measure.getDepth()));
addAttribute(builder,
" height: ",
String.format("%,d", data.measure.getHeight()));
addBreak(builder);
addAttribute(builder,
"start: ",
RefTime.nanosToString(data.measure.getRelativeBeginNanos())
+ " (" + toString(relativeNanosToDate(data.measure.getRelativeBeginNanos())) + ")");
addBreak(builder);
addAttribute(builder,
"end: ",
RefTime.nanosToString(data.measure.getRelativeEndOrCurrentNanos())
+ " (" + toString(relativeNanosToDate(data.measure.getRelativeEndNanos())) + ")");
addBreak(builder);
addAttribute(builder,
"elapsed: ",
RefTime.nanosToString(data.measure.getElapsedNanos()));
addBreak(builder);
if (data.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(data.measure.getStatus());
endBold(builder);
endFont(builder);
}
htmlStop(builder);
return builder.toString();
}
public static class PickedData {
public final Context context;
public final Measure measure;
public PickedData(Context context,
Measure measure) {
this.context = context;
this.measure = measure;
}
public boolean isContext() {
return context != null && measure == null;
}
public boolean isMeasure() {
return measure != null;
}
public boolean isVoid() {
return context == null;
}
}
public final PickedData 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 Measure pickedMeasure;
if (pickedLevel >= 0) {
final double t = xToTimeRelNanos(x);
final long epsilon = (long) (2.0 / this.pixelsPerNano);
pickedMeasure =
pickedContext.getMeasure((long) t, TimeMeasureMode.RELATIVE, epsilon, pickedLevel);
} else {
pickedMeasure = null;
}
return new PickedData(pickedContext, pickedMeasure);
}
public void setToolTip(PickedData data) {
setToolTipTextHtml(getToolTipTextHtml(data));
}
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 position The measure span position.
* @param y The vertical position.
*/
protected abstract void drawMeasure(Measure measure,
SpanPosition position,
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;
}
public final SpanPosition getPosition(Measure measure) {
return measure.getPosition(timeInfAbsNanos, timeSupAbsNanos);
}
/**
* Adds a measure.
*
* This measure will either be drawn or merged with other measures (synthesis).
*
* @param measure The measure.
* @param position The measure position.
*/
public final void addMeasure(Measure measure,
SpanPosition position) {
if (position != SpanPosition.OUTSIDE) {
if (measure.getStatus() == MeasureStatus.RUNNING || getDx(measure) >= 1.0) {
// Always draw running measures or measures that are long
// enough
flush();
drawMeasure(measure, position, y);
} else if (position != SpanPosition.OUTSIDE) {
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
final SpanPosition position = getPosition(last);
drawMeasure(last, position, y);
} else if (count > 1) {
// Synthesis
drawSynthesis();
}
count = 0;
last = null;
}
}
}