Autopsy  4.20.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
TimingMetricGraphPanel.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2018 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.healthmonitor;
20 
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;
43 
47 @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
48 class TimingMetricGraphPanel extends JPanel {
49 
50  private final static Logger logger = Logger.getLogger(TimingMetricGraphPanel.class.getName());
51 
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;
74 
75  TimingMetricGraphPanel(List<DatabaseTimingResult> timingResultsFull,
76  String hostName, boolean doLineGraph, String metricName, boolean skipOutliers, boolean showTrendLine) {
77 
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;
84  } else {
85  timingResults = timingResultsFull.stream()
86  .filter(t -> t.getHostName().equals(hostName))
87  .collect(Collectors.toList());
88  }
89 
90  if(showTrendLine) {
91  try {
92  trendLine = new TrendLine(timingResults);
93  } catch (HealthMonitorException ex) {
94  // Log it, set trendLine to null and continue on
95  logger.log(Level.WARNING, "Can not generate a trend line on empty data set");
96  trendLine = null;
97  }
98  }
99 
100  // Calculate these using the full data set, to make it easier to compare the results for
101  // individual hosts. Calculate the average at the same time.
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) {
108 
109  maxMetricTime = Math.max(maxMetricTime, result.getAverage());
110  minMetricTime = Math.min(minMetricTime, result.getAverage());
111 
112  maxTimestamp = Math.max(maxTimestamp, result.getTimestamp());
113  minTimestamp = Math.min(minTimestamp, result.getTimestamp());
114 
115  averageMetricTime += result.getAverage();
116  }
117  averageMetricTime = averageMetricTime / timingResultsFull.size();
118 
119  // If we're omitting outliers, we may use a different maxMetricTime.
120  // If the max time is reasonably close to the average, do nothing
121  if (this.skipOutliers && (maxMetricTime > (averageMetricTime * 5))) {
122  // Calculate the standard deviation
123  double intermediateValue = 0.0;
124  for (DatabaseTimingResult result : timingResultsFull) {
125  double diff = result.getAverage() - averageMetricTime;
126  intermediateValue += diff * diff;
127  }
128  double standardDeviation = Math.sqrt(intermediateValue / timingResultsFull.size());
129  maxMetricTime = averageMetricTime + standardDeviation;
130  }
131  }
132 
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 "})
151  @Override
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);
156 
157  // Get the max and min timestamps to create the x-axis.
158  // We add a small buffer to each side so the data won't overwrite the axes.
159  double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer
160  double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(2); // Two hour buffer
161 
162  // Get the max and min times to create the y-axis
163  // We add a small buffer to each side so the data won't overwrite the axes.
164  double maxValueOnYAxis = maxMetricTime;
165  double minValueOnYAxis = minMetricTime;
166  minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1));
167  maxValueOnYAxis = maxValueOnYAxis * 1.1;
168 
169  // The graph itself has the following corners:
170  // (padding + label padding, padding + font height) -> top left
171  // (padding + label padding, getHeight() - label padding - padding) -> bottom left
172  // (getWidth() - padding, padding + font height) -> top right
173  // (padding + label padding, getHeight() - label padding - padding) -> bottom right
174  int leftGraphPadding = padding + labelPadding;
175  int rightGraphPadding = padding;
176  int topGraphPadding = padding + g2.getFontMetrics().getHeight();
177  int bottomGraphPadding = labelPadding;
178 
179  // Calculate the scale for each axis.
180  // The size of the graph area is the width/height of the panel minus any padding.
181  // The scale is calculated based on this size of the graph compared to the data range.
182  // For example:
183  // getWidth() = 575 => graph width = 500
184  // If our max x value to plot is 10000 and our min is 0, then the xScale would be 0.05 - i.e.,
185  // our original x values will be multipled by 0.05 to translate them to an x-coordinate in the
186  // graph (plus the padding)
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);
191 
192  // Check if we should use a scale other than milliseconds
193  // The idea here is to pick the scale that would most commonly be used to
194  // represent the middle of our data. For example, if the middle of the graph
195  // would be 45,000,000 nanoseconds, then we would use milliseconds for the
196  // y-axis.
197  long middleOfGraphNano = (long)((minValueOnYAxis + (maxValueOnYAxis - minValueOnYAxis) / 2.0) * NANOSECONDS_PER_MILLISECOND);
198  double yLabelScale;
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();
207  yLabelScale = 1;
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));
214  } else {
215  yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_hours();
216  yLabelScale = 1.0 / (TimeUnit.HOURS.toMillis(1));
217  }
218 
219  // Draw white background
220  g2.setColor(Color.WHITE);
221  g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight);
222 
223  // Create hatch marks and grid lines for y axis.
224  int labelWidth;
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);
230  int y1 = y0;
231 
232  if ( ! timingResults.isEmpty()) {
233  // Draw the grid line
234  g2.setColor(gridColor);
235  g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1);
236 
237  // Create the label
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);
244 
245  // The nicest looking alignment for this label seems to be left-aligned with the top
246  // y-axis label. Save this position to be used to write the label later.
247  if (i == numberYDivisions) {
248  positionForMetricNameLabel = x0 - labelWidth - 5;
249  }
250  }
251 
252  // Draw the small hatch mark
253  g2.setColor(Color.BLACK);
254  g2.drawLine(x0, y0, x1, y1);
255  }
256 
257  // On the x-axis, the farthest right grid line should represent midnight preceding the last recorded value
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();
265 
266  // We don't want to display more than 20 grid lines. If we have more
267  // data then that, put multiple days within one division
268  long totalDays = (maxMidnightInMillis - (long)minValueOnXAxis) / MILLISECONDS_PER_DAY;
269  long daysPerDivision;
270  if(totalDays <= 20) {
271  daysPerDivision = 1;
272  } else {
273  daysPerDivision = (totalDays / 20);
274  if((totalDays % 20) != 0) {
275  daysPerDivision++;
276  }
277  }
278 
279  // Draw the vertical grid lines and labels
280  // The vertical grid lines will be at midnight, and display the date underneath them
281  // At present we use GMT because of some complications with daylight savings time.
282  for (long currentDivision = maxMidnightInMillis; currentDivision >= minValueOnXAxis; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) {
283 
284  int x0 = (int) ((currentDivision - minValueOnXAxis) * xScale + leftGraphPadding);
285  int x1 = x0;
286  int y0 = getHeight() - bottomGraphPadding;
287  int y1 = y0 - pointWidth;
288 
289  // Draw the light grey grid line
290  g2.setColor(gridColor);
291  g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding);
292 
293  // Draw the hatch mark
294  g2.setColor(Color.BLACK);
295  g2.drawLine(x0, y0, x1, y1);
296 
297  // Draw the label
298  Calendar thisDate = new GregorianCalendar();
299  thisDate.setTimeZone(TimeZone.getTimeZone("GMT")); // Stick with GMT to avoid daylight savings issues
300  thisDate.setTimeInMillis(currentDivision);
301  int month = thisDate.get(Calendar.MONTH) + 1;
302  int day = thisDate.get(Calendar.DAY_OF_MONTH);
303 
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);
308  }
309 
310  // Create x and y axes
311  g2.setColor(Color.BLACK);
312  g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding);
313  g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding);
314 
315  // Create the points to plot
316  List<Point> graphPoints = new ArrayList<>();
317  for (int i = 0; i < timingResults.size(); i++) {
318  double metricTime = timingResults.get(i).getAverage();
319 
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));
323  }
324 
325  // Sort the points
326  Collections.sort(graphPoints, new Comparator<Point>() {
327  @Override
328  public int compare(Point o1, Point o2) {
329  if(o1.getX() > o2.getX()) {
330  return 1;
331  } else if (o1.getX() < o2.getX()) {
332  return -1;
333  }
334  return 0;
335  }
336  });
337 
338  // Draw the selected type of graph. If there's only one data point,
339  // draw that single point.
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);
349  }
350  } else {
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);
357  }
358  }
359 
360  // Draw the trend line.
361  // Don't draw anything if we don't have at least two data points.
362  if(showTrendLine && (trendLine != null) && (timingResults.size() > 1)) {
363  double x0value = minValueOnXAxis;
364  double y0value = trendLine.getExpectedValueAt(x0value);
365  if (y0value < minValueOnYAxis) {
366  try {
367  y0value = minValueOnYAxis;
368  x0value = trendLine.getXGivenY(y0value);
369  } catch (HealthMonitorException ex) {
370  // The exception is caused by a slope of zero on the trend line, which
371  // shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
372  // If it does, log a warning but continue on with the original values.
373  logger.log(Level.WARNING, "Error plotting trend line", ex);
374  }
375  } else if (y0value > maxValueOnYAxis) {
376  try {
377  y0value = maxValueOnYAxis;
378  x0value = trendLine.getXGivenY(y0value);
379  } catch (HealthMonitorException ex) {
380  // The exception is caused by a slope of zero on the trend line, which
381  // shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
382  // If it does, log a warning but continue on with the original values.
383  logger.log(Level.WARNING, "Error plotting trend line", ex);
384  }
385  }
386 
387  int x0 = (int) ((x0value - minValueOnXAxis) * xScale) + leftGraphPadding;
388  int y0 = (int) ((maxValueOnYAxis - y0value) * yScale + topGraphPadding);
389 
390  double x1value = maxValueOnXAxis;
391  double y1value = trendLine.getExpectedValueAt(maxValueOnXAxis);
392  if (y1value < minValueOnYAxis) {
393  try {
394  y1value = minValueOnYAxis;
395  x1value = trendLine.getXGivenY(y1value);
396  } catch (HealthMonitorException ex) {
397  // The exception is caused by a slope of zero on the trend line, which
398  // shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
399  // If it does, log a warning but continue on with the original values.
400  logger.log(Level.WARNING, "Error plotting trend line", ex);
401  }
402  } else if (y1value > maxValueOnYAxis) {
403  try {
404  y1value = maxValueOnYAxis;
405  x1value = trendLine.getXGivenY(y1value);
406  } catch (HealthMonitorException ex) {
407  // The exception is caused by a slope of zero on the trend line, which
408  // shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
409  // If it does, log a warning but continue on with the original values.
410  logger.log(Level.WARNING, "Error plotting trend line", ex);
411  }
412  }
413 
414  int x1 = (int) ((x1value - minValueOnXAxis) * xScale) + leftGraphPadding;
415  int y1 = (int) ((maxValueOnYAxis - y1value) * yScale + topGraphPadding);
416 
417  g2.setStroke(GRAPH_STROKE);
418  g2.setColor(trendLineColor);
419  g2.drawLine(x0, y0, x1, y1);
420  }
421 
422  // The graph lines may have extended up past the bounds of the graph. Overwrite that
423  // area with the original background color.
424  g2.setColor(this.getBackground());
425  g2.fillRect(leftGraphPadding, 0, graphWidth, topGraphPadding);
426 
427  // Write the scale. Do this after we erase the top block of the graph.
428  g2.setColor(Color.BLACK);
429  String scaleStr = Bundle.TimingMetricGraphPanel_paintComponent_displayingTime() + yUnitString;
430  String titleStr = metricName + " - " + scaleStr;
431  g2.drawString(titleStr, positionForMetricNameLabel, padding);
432  }
433 
445  private class TrendLine {
446 
447  double slope;
448  double yInt;
449 
450  TrendLine(List<DatabaseTimingResult> timingResults) throws HealthMonitorException {
451 
452  if((timingResults == null) || timingResults.isEmpty()) {
453  throw new HealthMonitorException("Can not generate trend line for empty/null data set");
454  }
455 
456  // Calculate intermediate values
457  int n = timingResults.size();
458  double sumX = 0;
459  double sumY = 0;
460  double sumXY = 0;
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();
465 
466  sumX += x;
467  sumY += y;
468  sumXY += x * y;
469  sumXsquared += x * x;
470  }
471 
472  // Calculate slope
473  // With only one measurement, the denominator will end being zero in the formula.
474  // Use a horizontal line in this case (or any case where the denominator is zero)
475  double denominator = n * sumXsquared - sumX * sumX;
476  if (denominator != 0) {
477  slope = (n * sumXY - sumX * sumY) / denominator;
478  } else {
479  slope = 0;
480  }
481 
482  // Calculate y intercept
483  yInt = (sumY - slope * sumX) / n;
484  }
485 
491  double getExpectedValueAt(double x) {
492  return (slope * x + yInt);
493  }
494 
502  double getXGivenY(double y) throws HealthMonitorException {
503  if (slope != 0.0) {
504  return ((y - yInt) / slope);
505  } else {
506  throw new HealthMonitorException("Attempted division by zero in trend line calculation");
507  }
508  }
509  }
510 
511 }

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.