Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.healthmonitor;
20
21import java.awt.BasicStroke;
22import java.awt.Color;
23import java.awt.FontMetrics;
24import java.awt.Graphics;
25import java.awt.Graphics2D;
26import java.awt.Point;
27import java.awt.RenderingHints;
28import java.awt.Stroke;
29import java.util.Collections;
30import java.util.stream.Collectors;
31import java.util.Comparator;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.Calendar;
35import java.util.GregorianCalendar;
36import javax.swing.JPanel;
37import org.sleuthkit.autopsy.coreutils.Logger;
38import java.util.logging.Level;
39import java.util.TimeZone;
40import java.util.concurrent.TimeUnit;
41import org.openide.util.NbBundle;
42import org.sleuthkit.autopsy.healthmonitor.HealthMonitor.DatabaseTimingResult;
43
47@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
48class 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}
synchronized static Logger getLogger(String name)
Definition Logger.java:124

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.