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");