19 package org.sleuthkit.autopsy.healthmonitor;
 
   21 import java.awt.BasicStroke;
 
   22 import java.awt.Color;
 
   23 import java.awt.FontMetrics;
 
   24 import java.awt.Graphics;
 
   25 import java.awt.Graphics2D;
 
   26 import java.awt.Point;
 
   27 import java.awt.RenderingHints;
 
   28 import java.awt.Stroke;
 
   29 import java.util.Collections;
 
   30 import java.util.stream.Collectors;
 
   31 import java.util.Comparator;
 
   32 import java.util.ArrayList;
 
   33 import java.util.List;
 
   34 import java.util.Calendar;
 
   35 import java.util.GregorianCalendar;
 
   36 import javax.swing.JPanel;
 
   38 import java.util.logging.Level;
 
   39 import java.util.TimeZone;
 
   40 import java.util.concurrent.TimeUnit;
 
   41 import org.openide.util.NbBundle;
 
   47 @SuppressWarnings(
"PMD.SingularField") 
 
   48 class TimingMetricGraphPanel extends JPanel {
 
   50     private final static Logger logger = Logger.getLogger(TimingMetricGraphPanel.class.getName());
 
   52     private final int padding = 25;
 
   53     private final int labelPadding = 25;
 
   54     private final Color lineColor = 
new Color(0x12, 0x20, 0xdb, 180);
 
   55     private final Color gridColor = 
new Color(200, 200, 200, 200);
 
   56     private final Color trendLineColor = 
new Color(150, 10, 10, 200);
 
   57     private static final Stroke GRAPH_STROKE = 
new BasicStroke(2f);
 
   58     private static final Stroke NARROW_STROKE = 
new BasicStroke(1f);
 
   59     private final int pointWidth = 4;
 
   60     private final int numberYDivisions = 10;
 
   61     private List<DatabaseTimingResult> timingResults;
 
   62     private final String metricName;
 
   63     private final boolean doLineGraph;
 
   64     private final boolean skipOutliers;
 
   65     private final boolean showTrendLine;
 
   66     private String yUnitString;
 
   67     private TrendLine trendLine;
 
   68     private final long MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
 
   69     private final long NANOSECONDS_PER_MILLISECOND = 1000 * 1000;
 
   70     private long maxTimestamp;
 
   71     private long minTimestamp;
 
   72     private double maxMetricTime;
 
   73     private double minMetricTime;
 
   75     TimingMetricGraphPanel(List<DatabaseTimingResult> timingResultsFull, 
 
   76             String hostName, 
boolean doLineGraph, String metricName, 
boolean skipOutliers, 
boolean showTrendLine) {
 
   78         this.doLineGraph = doLineGraph;
 
   79         this.skipOutliers = skipOutliers;
 
   80         this.showTrendLine = showTrendLine;
 
   81         this.metricName = metricName;
 
   82         if(hostName == null || hostName.isEmpty()) {
 
   83             timingResults = timingResultsFull;
 
   85             timingResults = timingResultsFull.stream()
 
   86                             .filter(t -> t.getHostName().equals(hostName))
 
   87                             .collect(Collectors.toList());
 
   92                 trendLine = 
new TrendLine(timingResults);
 
   93             } 
catch (HealthMonitorException ex) {
 
   95                 logger.log(Level.WARNING, 
"Can not generate a trend line on empty data set");
 
  102         maxMetricTime = Double.MIN_VALUE;
 
  103         minMetricTime = Double.MAX_VALUE;
 
  104         maxTimestamp = Long.MIN_VALUE;
 
  105         minTimestamp = Long.MAX_VALUE;
 
  106         double averageMetricTime = 0.0;
 
  107         for (DatabaseTimingResult result : timingResultsFull) {
 
  109             maxMetricTime = Math.max(maxMetricTime, result.getAverage());
 
  110             minMetricTime = Math.min(minMetricTime, result.getAverage());
 
  112             maxTimestamp = Math.max(maxTimestamp, result.getTimestamp());
 
  113             minTimestamp = Math.min(minTimestamp, result.getTimestamp());
 
  115             averageMetricTime += result.getAverage();
 
  117         averageMetricTime = averageMetricTime / timingResultsFull.size();
 
  121         if (this.skipOutliers && (maxMetricTime > (averageMetricTime * 5))) {
 
  123             double intermediateValue = 0.0;
 
  124             for (DatabaseTimingResult result : timingResultsFull) {
 
  125                 double diff = result.getAverage() - averageMetricTime;
 
  126                 intermediateValue += diff * diff;
 
  128             double standardDeviation = Math.sqrt(intermediateValue / timingResultsFull.size());
 
  129             maxMetricTime = averageMetricTime + standardDeviation;
 
  144     @NbBundle.Messages({
"TimingMetricGraphPanel.paintComponent.nanoseconds=nanoseconds",
 
  145                         "TimingMetricGraphPanel.paintComponent.microseconds=microseconds",
 
  146                         "TimingMetricGraphPanel.paintComponent.milliseconds=milliseconds",
 
  147                         "TimingMetricGraphPanel.paintComponent.seconds=seconds",
 
  148                         "TimingMetricGraphPanel.paintComponent.minutes=minutes",
 
  149                         "TimingMetricGraphPanel.paintComponent.hours=hours",
 
  150                         "TimingMetricGraphPanel.paintComponent.displayingTime=displaying time in "})
 
  152     protected void paintComponent(Graphics g) {
 
  153         super.paintComponent(g);
 
  154         Graphics2D g2 = (Graphics2D) g;
 
  155         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
 
  159         double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); 
 
  160         double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(2); 
 
  164         double maxValueOnYAxis = maxMetricTime;
 
  165         double minValueOnYAxis = minMetricTime;  
 
  166         minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1));
 
  167         maxValueOnYAxis = maxValueOnYAxis * 1.1;
 
  174         int leftGraphPadding = padding + labelPadding;
 
  175         int rightGraphPadding = padding;
 
  176         int topGraphPadding = padding + g2.getFontMetrics().getHeight();
 
  177         int bottomGraphPadding = labelPadding;
 
  187         int graphWidth = getWidth() - leftGraphPadding - rightGraphPadding;
 
  188         int graphHeight = getHeight() - topGraphPadding - bottomGraphPadding;
 
  189         double xScale = ((double) graphWidth) / (maxValueOnXAxis - minValueOnXAxis);
 
  190         double yScale = ((double) graphHeight) / (maxValueOnYAxis - minValueOnYAxis);  
 
  197         long middleOfGraphNano = (long)((minValueOnYAxis + (maxValueOnYAxis - minValueOnYAxis) / 2.0) * NANOSECONDS_PER_MILLISECOND);
 
  199         if(middleOfGraphNano < TimeUnit.MICROSECONDS.toNanos(1)) {
 
  200             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_nanoseconds();
 
  201             yLabelScale = TimeUnit.MILLISECONDS.toNanos(1);
 
  202         } 
else if (TimeUnit.NANOSECONDS.toMicros(middleOfGraphNano) < TimeUnit.MILLISECONDS.toMicros(1)) {
 
  203             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_microseconds();
 
  204             yLabelScale =  TimeUnit.MILLISECONDS.toMicros(1);
 
  205         } 
else if (TimeUnit.NANOSECONDS.toMillis(middleOfGraphNano) < TimeUnit.SECONDS.toMillis(1)) {
 
  206             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_milliseconds();
 
  208         } 
else if (TimeUnit.NANOSECONDS.toSeconds(middleOfGraphNano) < TimeUnit.MINUTES.toSeconds(1)) {
 
  209             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_seconds();
 
  210             yLabelScale = 1.0 / TimeUnit.SECONDS.toMillis(1);
 
  211         } 
else if (TimeUnit.NANOSECONDS.toMinutes(middleOfGraphNano) < TimeUnit.HOURS.toMinutes(1)) {
 
  212             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_minutes();
 
  213             yLabelScale = 1.0 / (TimeUnit.MINUTES.toMillis(1));
 
  215             yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_hours();
 
  216             yLabelScale = 1.0 / (TimeUnit.HOURS.toMillis(1));
 
  220         g2.setColor(Color.WHITE);
 
  221         g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight); 
 
  225         int positionForMetricNameLabel = 0;
 
  226         for (
int i = 0; i < numberYDivisions + 1; i++) {
 
  227             int x0 = leftGraphPadding;
 
  228             int x1 = pointWidth + leftGraphPadding;
 
  229             int y0 = getHeight() - ((i * graphHeight) / numberYDivisions + bottomGraphPadding);
 
  232             if ( ! timingResults.isEmpty()) {
 
  234                 g2.setColor(gridColor);
 
  235                 g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1);
 
  238                 g2.setColor(Color.BLACK);
 
  239                 double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions));
 
  240                 String yLabel = Double.toString(((
int) (yValue * 100 * yLabelScale)) / 100.0);
 
  241                 FontMetrics fontMetrics = g2.getFontMetrics();
 
  242                 labelWidth = fontMetrics.stringWidth(yLabel);
 
  243                 g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3);
 
  247                 if (i == numberYDivisions) {
 
  248                     positionForMetricNameLabel = x0 - labelWidth - 5;
 
  253             g2.setColor(Color.BLACK);
 
  254             g2.drawLine(x0, y0, x1, y1);
 
  258         Calendar maxDate = 
new GregorianCalendar();
 
  259         maxDate.setTimeInMillis(maxTimestamp);
 
  260         maxDate.set(Calendar.HOUR_OF_DAY, 0);
 
  261         maxDate.set(Calendar.MINUTE, 0);
 
  262         maxDate.set(Calendar.SECOND, 0);
 
  263         maxDate.set(Calendar.MILLISECOND, 0);
 
  264         long maxMidnightInMillis = maxDate.getTimeInMillis();
 
  268         long totalDays = (maxMidnightInMillis - (long)minValueOnXAxis) / MILLISECONDS_PER_DAY;
 
  269         long daysPerDivision;
 
  270         if(totalDays <= 20) {
 
  273             daysPerDivision = (totalDays / 20);
 
  274             if((totalDays % 20) != 0) {
 
  282         for (
long currentDivision = maxMidnightInMillis; currentDivision >= minValueOnXAxis; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) {
 
  284             int x0 = (int) ((currentDivision - minValueOnXAxis) * xScale + leftGraphPadding);
 
  286             int y0 = getHeight() - bottomGraphPadding;
 
  287             int y1 = y0 - pointWidth;
 
  290             g2.setColor(gridColor);
 
  291             g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding);
 
  294             g2.setColor(Color.BLACK);
 
  295             g2.drawLine(x0, y0, x1, y1);
 
  298             Calendar thisDate = 
new GregorianCalendar();
 
  299             thisDate.setTimeZone(TimeZone.getTimeZone(
"GMT")); 
 
  300             thisDate.setTimeInMillis(currentDivision);
 
  301             int month = thisDate.get(Calendar.MONTH) + 1;
 
  302             int day = thisDate.get(Calendar.DAY_OF_MONTH);
 
  304             String xLabel = month + 
"/" + day;
 
  305             FontMetrics metrics = g2.getFontMetrics();
 
  306             labelWidth = metrics.stringWidth(xLabel);
 
  307             g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3);           
 
  311         g2.setColor(Color.BLACK);
 
  312         g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding);
 
  313         g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding);
 
  316         List<Point> graphPoints = 
new ArrayList<>();
 
  317         for (
int i = 0; i < timingResults.size(); i++) {     
 
  318             double metricTime = timingResults.get(i).getAverage();
 
  320             int x1 = (int) ((timingResults.get(i).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding);
 
  321             int y1 = (int) ((maxValueOnYAxis - metricTime) * yScale + topGraphPadding);
 
  322             graphPoints.add(
new Point(x1, y1));   
 
  326         Collections.sort(graphPoints, 
new Comparator<Point>() {
 
  328             public int compare(Point o1, Point o2) {
 
  329                 if(o1.getX() > o2.getX()) {
 
  331                 } 
else if (o1.getX() < o2.getX()) {
 
  340         g2.setStroke(NARROW_STROKE);
 
  341         g2.setColor(lineColor);
 
  342         if(doLineGraph && graphPoints.size() > 1) {
 
  343             for (
int i = 0; i < graphPoints.size() - 1; i++) {
 
  344                 int x1 = graphPoints.get(i).x;
 
  345                 int y1 = graphPoints.get(i).y;
 
  346                 int x2 = graphPoints.get(i + 1).x;
 
  347                 int y2 = graphPoints.get(i + 1).y;
 
  348                 g2.drawLine(x1, y1, x2, y2);
 
  351             for (
int i = 0; i < graphPoints.size(); i++) {
 
  352                 int x = graphPoints.get(i).x - pointWidth / 2;
 
  353                 int y = graphPoints.get(i).y - pointWidth / 2;
 
  354                 int ovalW = pointWidth;
 
  355                 int ovalH = pointWidth;
 
  356                 g2.fillOval(x, y, ovalW, ovalH);
 
  362         if(showTrendLine && (trendLine != null) && (timingResults.size() > 1)) {
 
  363             double x0value = minValueOnXAxis;
 
  364             double y0value = trendLine.getExpectedValueAt(x0value);
 
  365             if (y0value < minValueOnYAxis) {
 
  367                     y0value = minValueOnYAxis;
 
  368                     x0value = trendLine.getXGivenY(y0value);
 
  369                 } 
catch (HealthMonitorException ex) {
 
  373                     logger.log(Level.WARNING, 
"Error plotting trend line", ex);
 
  375             } 
else if (y0value > maxValueOnYAxis) {
 
  377                     y0value = maxValueOnYAxis;
 
  378                     x0value = trendLine.getXGivenY(y0value);
 
  379                 } 
catch (HealthMonitorException ex) {
 
  383                     logger.log(Level.WARNING, 
"Error plotting trend line", ex);
 
  387             int x0 = (int) ((x0value - minValueOnXAxis) * xScale) + leftGraphPadding;
 
  388             int y0 = (int) ((maxValueOnYAxis - y0value) * yScale + topGraphPadding);
 
  390             double x1value = maxValueOnXAxis;
 
  391             double y1value = trendLine.getExpectedValueAt(maxValueOnXAxis);
 
  392             if (y1value < minValueOnYAxis) {
 
  394                     y1value = minValueOnYAxis;
 
  395                     x1value = trendLine.getXGivenY(y1value);
 
  396                 } 
catch (HealthMonitorException ex) {
 
  400                     logger.log(Level.WARNING, 
"Error plotting trend line", ex);
 
  402             } 
else if (y1value > maxValueOnYAxis) {
 
  404                     y1value = maxValueOnYAxis;
 
  405                     x1value = trendLine.getXGivenY(y1value);
 
  406                 } 
catch (HealthMonitorException ex) {
 
  410                     logger.log(Level.WARNING, 
"Error plotting trend line", ex);
 
  414             int x1 = (int) ((x1value - minValueOnXAxis) * xScale) + leftGraphPadding;
 
  415             int y1 = (int) ((maxValueOnYAxis - y1value) * yScale + topGraphPadding);
 
  417             g2.setStroke(GRAPH_STROKE);
 
  418             g2.setColor(trendLineColor);
 
  419             g2.drawLine(x0, y0, x1, y1);
 
  424         g2.setColor(this.getBackground());
 
  425         g2.fillRect(leftGraphPadding, 0, graphWidth, topGraphPadding); 
 
  428         g2.setColor(Color.BLACK);
 
  429         String scaleStr = Bundle.TimingMetricGraphPanel_paintComponent_displayingTime() + yUnitString;
 
  430         String titleStr = metricName + 
" - " + scaleStr;
 
  431         g2.drawString(titleStr, positionForMetricNameLabel, padding);
 
  450         TrendLine(List<DatabaseTimingResult> timingResults) 
throws HealthMonitorException {
 
  452             if((timingResults == null) || timingResults.isEmpty()) {
 
  453                 throw new HealthMonitorException(
"Can not generate trend line for empty/null data set");
 
  457             int n = timingResults.size();
 
  461             double sumXsquared = 0;
 
  462             for(
int i = 0;i < n;i++) {
 
  463                 double x = timingResults.get(i).getTimestamp();
 
  464                 double y = timingResults.get(i).getAverage();
 
  469                 sumXsquared += x * x;
 
  475             double denominator = n * sumXsquared - sumX * sumX;
 
  476             if (denominator != 0) {
 
  477                 slope = (n * sumXY - sumX * sumY) / denominator;
 
  483             yInt = (sumY - slope * sumX) / n;
 
  491         double getExpectedValueAt(
double x) {
 
  492             return (slope * x + yInt);
 
  502         double getXGivenY(
double y) 
throws HealthMonitorException {
 
  504                 return ((y - yInt) / slope);
 
  506                 throw new HealthMonitorException(
"Attempted division by zero in trend line calculation");